@electerm/electerm-react 3.15.198 → 5.0.7

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 (32) hide show
  1. package/client/common/constants.js +2 -1
  2. package/client/common/fetch-from-server.js +12 -2
  3. package/client/components/common/drawer.styl +1 -1
  4. package/client/components/footer/footer-entry.jsx +24 -4
  5. package/client/components/footer/footer.styl +1 -1
  6. package/client/components/layout/layout.jsx +13 -8
  7. package/client/components/main/main.jsx +6 -2
  8. package/client/components/quick-commands/qm.styl +1 -1
  9. package/client/components/quick-commands/quick-commands-box.jsx +3 -2
  10. package/client/components/session/session.jsx +1 -1
  11. package/client/components/session/sessions.jsx +1 -1
  12. package/client/components/setting-panel/setting-modal.jsx +1 -1
  13. package/client/components/setting-panel/setting-wrap.jsx +1 -2
  14. package/client/components/setting-panel/setting-wrap.styl +1 -1
  15. package/client/components/setting-panel/setting.styl +1 -1
  16. package/client/components/sftp/sftp-entry.jsx +36 -55
  17. package/client/components/sidebar/bookmark-select.jsx +2 -2
  18. package/client/components/sidebar/index.jsx +23 -7
  19. package/client/components/sidebar/side-panel.jsx +3 -3
  20. package/client/components/sidebar/sidebar.styl +6 -1
  21. package/client/components/terminal/shell.js +10 -2
  22. package/client/components/terminal/terminal-apis.js +4 -4
  23. package/client/components/terminal/terminal-command-dropdown.jsx +1 -0
  24. package/client/components/terminal/terminal.jsx +50 -17
  25. package/client/components/tree-list/tree-list-row.jsx +2 -2
  26. package/client/components/tree-list/tree-list.jsx +1 -1
  27. package/client/components/vnc/vnc-session.jsx +65 -2
  28. package/client/css/mobile.styl +2 -1
  29. package/client/store/common.js +15 -3
  30. package/client/store/init-state.js +9 -2
  31. package/client/store/store.js +12 -3
  32. package/package.json +1 -1
@@ -147,8 +147,9 @@ export const terminalFtpType = 'ftp'
147
147
  export const terminalSpiceType = 'spice'
148
148
  export const openedSidebarKey = 'opened-sidebar'
149
149
  export const sidebarPinnedKey = 'sidebar-pinned'
150
+ export const leftSideBarOpenKey = 'left-side-bar-open'
150
151
  export const pinnedQuickCommandBarKey = 'pinned-quick-command-bar'
151
- export const leftSidebarWidthKey = 'left-sidebar-width'
152
+ export const leftSidePanelWidthKey = 'left-sidebar-width'
152
153
  export const rightSidebarWidthKey = 'right-sidebar-width'
153
154
  export const addPanelWidthLsKey = 'addPanelWidth'
154
155
  export const sftpDefaultSortSettingKey = 'sftp-default-sort'
@@ -27,7 +27,8 @@ export const initWsCommon = async () => {
27
27
 
28
28
  window.pre.ipcOnEvent('power-resume', initWsCommon)
29
29
 
30
- const wsFetch = async (data) => {
30
+ const wsFetch = async (data, options = {}) => {
31
+ const { silent = false } = options
31
32
  if (!window.et.wsOpened) {
32
33
  await initWsCommon()
33
34
  }
@@ -35,7 +36,16 @@ const wsFetch = async (data) => {
35
36
  return new NewPromise((resolve, reject) => {
36
37
  window.et.commonWs.once((arg) => {
37
38
  if (arg.error) {
38
- console.error('fetch error', arg.error)
39
+ // Rejections are propagated to the caller via the returned promise, so
40
+ // callers decide severity. Many fetches (shell detection, owner lookup,
41
+ // folder size) are best-effort and catch gracefully; logging every one
42
+ // at error level with a stack dump surfaces scary-but-harmless failures
43
+ // (e.g. an SSH server rejecting an auxiliary exec channel). Default to a
44
+ // low-key warning carrying only the message; best-effort probes pass
45
+ // { silent: true } to suppress this and log their own clearer message.
46
+ if (!silent) {
47
+ console.warn('fetch error:', arg.error.message || arg.error)
48
+ }
39
49
  return reject(new Error(arg.error.message))
40
50
  }
41
51
  resolve(arg.data)
@@ -1,7 +1,7 @@
1
1
  .custom-drawer
2
2
  position fixed
3
3
  top 0
4
- left 43px
4
+ left var(--left-side-bar-width, 43px)
5
5
  right 0
6
6
  bottom 0
7
7
 
@@ -3,7 +3,7 @@ import {
3
3
  Select,
4
4
  Dropdown
5
5
  } from 'antd'
6
- import { InfoCircleOutlined, TranslationOutlined } from '@ant-design/icons'
6
+ import { InfoCircleOutlined, TranslationOutlined, DoubleRightOutlined } from '@ant-design/icons'
7
7
  import './footer.styl'
8
8
  import { statusMap } from '../../common/constants'
9
9
  import BatchInput from './batch-input'
@@ -164,12 +164,29 @@ export default auto(function FooterEntry (props) {
164
164
  )
165
165
  }
166
166
 
167
+ function handleShowSidebar () {
168
+ window.store.toggleLeftSideBar()
169
+ }
170
+
167
171
  const {
168
- leftSidebarWidth,
172
+ leftSidePanelWidth,
173
+ leftSideBarWidth,
169
174
  openedSideBar,
170
175
  inActiveTerminal
171
176
  } = props.store
172
- const w = 43 + leftSidebarWidth
177
+ const w = leftSideBarWidth + leftSidePanelWidth
178
+ // icon bar hidden: show a control on the left of the footer to bring the
179
+ // sidebar back
180
+ const showSidebarIcon = leftSideBarWidth === 0
181
+ ? (
182
+ <div className='terminal-footer-unit terminal-footer-show-sidebar'>
183
+ <DoubleRightOutlined
184
+ className='pointer font18 show-sidebar-icon'
185
+ onClick={handleShowSidebar}
186
+ />
187
+ </div>
188
+ )
189
+ : null
173
190
  const sideProps = openedSideBar
174
191
  ? {
175
192
  className: 'main-footer',
@@ -184,12 +201,15 @@ export default auto(function FooterEntry (props) {
184
201
  !inActiveTerminal
185
202
  ) {
186
203
  return (
187
- <div className='main-footer' {...sideProps} />
204
+ <div className='main-footer' {...sideProps}>
205
+ {showSidebarIcon}
206
+ </div>
188
207
  )
189
208
  }
190
209
  return (
191
210
  <div {...sideProps}>
192
211
  <div className='terminal-footer-flex'>
212
+ {showSidebarIcon}
193
213
  {!isAIDisabled() && renderAIIcon()}
194
214
  {renderCmdHistory()}
195
215
  {renderQuickCommands()}
@@ -2,7 +2,7 @@
2
2
  background var(--main)
3
3
  height 36px
4
4
  position absolute
5
- left 44px
5
+ left calc(var(--left-side-bar-width, 43px) + 1px)
6
6
  right 0
7
7
  bottom 0
8
8
  z-index 200
@@ -33,7 +33,8 @@ export default auto(function Layout (props) {
33
33
  height,
34
34
  pinnedQuickCommandBar,
35
35
  // tabsHeight,
36
- leftSidebarWidth,
36
+ leftSidePanelWidth,
37
+ leftSideBarWidth,
37
38
  // infoPanelPinned,
38
39
  pinned,
39
40
  rightPanelVisible,
@@ -43,7 +44,7 @@ export default auto(function Layout (props) {
43
44
  inActiveTerminal
44
45
  } = props.store
45
46
  const h = height - footerHeight - (inActiveTerminal && pinnedQuickCommandBar ? quickCommandBoxHeight : 0) + resizeTrigger
46
- const l = pinned ? 43 + leftSidebarWidth : 43
47
+ const l = pinned ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
47
48
  const r = rightPanelVisible && rightPanelPinned ? rightPanelWidth : 0
48
49
  return {
49
50
  height: h,
@@ -59,15 +60,18 @@ export default auto(function Layout (props) {
59
60
  height,
60
61
  width,
61
62
  pinnedQuickCommandBar,
62
- leftSidebarWidth,
63
+ leftSidePanelWidth,
64
+ leftSideBarWidth,
63
65
  rightPanelVisible,
64
66
  rightPanelPinned,
65
67
  rightPanelWidth,
66
68
  pinned
67
69
  } = props.store
68
- const l = pinned ? leftSidebarWidth : 0
70
+ const l = pinned ? leftSidePanelWidth : 0
69
71
  const r = rightPanelPinned && rightPanelVisible ? rightPanelWidth : 0
70
- const w = width - l - r - 42
72
+ // account for the far-left icon bar (sidebarWidth - 1px border on desktop;
73
+ // 0 when the bar is hidden on mobile)
74
+ const w = width - l - r - (leftSideBarWidth > 0 ? leftSideBarWidth - 1 : 0)
71
75
  const h = height - footerHeight - (pinnedQuickCommandBar ? quickCommandBoxHeight : 0)
72
76
  return layoutAlg(layout, w, h)
73
77
  }
@@ -115,7 +119,7 @@ export default auto(function Layout (props) {
115
119
  'pinnedQuickCommandBar',
116
120
  'tabsHeight',
117
121
  'appPath',
118
- 'leftSidebarWidth',
122
+ 'leftSidePanelWidth',
119
123
  'addPanelWidth',
120
124
  'pinned',
121
125
  'openedSideBar',
@@ -153,7 +157,8 @@ export default auto(function Layout (props) {
153
157
  'pinnedQuickCommandBar',
154
158
  'qmSortByFrequency',
155
159
  'inActiveTerminal',
156
- 'leftSidebarWidth',
160
+ 'leftSidePanelWidth',
161
+ 'leftSideBarWidth',
157
162
  'openedSideBar',
158
163
  'currentQuickCommands'
159
164
  ])
@@ -177,7 +182,7 @@ export default auto(function Layout (props) {
177
182
  'pinnedQuickCommandBar',
178
183
  'tabsHeight',
179
184
  'appPath',
180
- 'leftSidebarWidth',
185
+ 'leftSidePanelWidth',
181
186
  'pinned',
182
187
  'openedSideBar',
183
188
  'config',
@@ -119,7 +119,10 @@ export default auto(function Index (props) {
119
119
  'is-desktop': !store.isMobile
120
120
  })
121
121
  const ext1 = {
122
- className: cls
122
+ className: cls,
123
+ style: {
124
+ '--left-side-bar-width': store.leftSideBarWidth + 'px'
125
+ }
123
126
  }
124
127
  // Get active tab IDs
125
128
  const activeTabIds = [
@@ -168,7 +171,8 @@ export default auto(function Index (props) {
168
171
  'settingTab',
169
172
  'settingItem',
170
173
  'isSyncingSetting',
171
- 'leftSidebarWidth',
174
+ 'leftSidePanelWidth',
175
+ 'leftSideBarWidth',
172
176
  'transferTab',
173
177
  'sidebarPanelTab',
174
178
  'openWidgetsModal'
@@ -5,7 +5,7 @@
5
5
  .qm-wrap-tooltip
6
6
  background var(--main)
7
7
  position absolute
8
- left 43px
8
+ left var(--left-side-bar-width, 43px)
9
9
  right 0
10
10
  z-index 89
11
11
  height auto
@@ -176,7 +176,8 @@ export default function QuickCommandsFooterBox (props) {
176
176
  pinnedQuickCommandBar,
177
177
  qmSortByFrequency,
178
178
  inActiveTerminal,
179
- leftSidebarWidth,
179
+ leftSidePanelWidth,
180
+ leftSideBarWidth,
180
181
  openedSideBar
181
182
  } = props
182
183
  if ((!openQuickCommandBar && !pinnedQuickCommandBar) || !inActiveTerminal) {
@@ -203,7 +204,7 @@ export default function QuickCommandsFooterBox (props) {
203
204
  : 'text'
204
205
  const cls = classNames('qm-list-wrap')
205
206
  const type = qmSortByFrequency ? 'primary' : 'default'
206
- const w = openedSideBar ? 43 + leftSidebarWidth : 43
207
+ const w = openedSideBar ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
207
208
  const qmProps = {
208
209
  className: 'qm-wrap-tooltip',
209
210
  style: {
@@ -278,7 +278,7 @@ export default class SessionWrapper extends Component {
278
278
  'height',
279
279
  'width',
280
280
  'tabsHeight',
281
- 'leftSidebarWidth',
281
+ 'leftSidePanelWidth',
282
282
  'pinned',
283
283
  'openedSideBar',
284
284
  'delTab',
@@ -74,7 +74,7 @@ export default class Sessions extends Component {
74
74
  'pinnedQuickCommandBar',
75
75
  'tabsHeight',
76
76
  'appPath',
77
- 'leftSidebarWidth',
77
+ 'leftSidePanelWidth',
78
78
  'pinned',
79
79
  'openedSideBar',
80
80
  'fullscreen'
@@ -70,7 +70,7 @@ export default auto(function SettingModalWrap (props) {
70
70
  'config',
71
71
  'checkedKeys',
72
72
  'expandedKeys',
73
- 'leftSidebarWidth',
73
+ 'leftSidePanelWidth',
74
74
  'initLoadingData'
75
75
  ])
76
76
  }
@@ -5,7 +5,6 @@
5
5
  import { Component } from 'react'
6
6
  import Drawer from '../common/drawer'
7
7
  import { CloseCircleOutlined } from '@ant-design/icons'
8
- import { sidebarWidth } from '../../common/constants'
9
8
  import AppDrag from '../tabs/app-drag'
10
9
  import './setting-wrap.styl'
11
10
 
@@ -33,7 +32,7 @@ export default class SettingWrap extends Component {
33
32
  open: this.props.visible,
34
33
  onClose: this.props.onCancel,
35
34
  className: 'setting-wrap',
36
- size: this.props.innerWidth - sidebarWidth,
35
+ size: this.props.innerWidth - window.store.leftSideBarWidth,
37
36
  zIndex: 888,
38
37
  placement: 'left'
39
38
  }
@@ -72,7 +72,7 @@
72
72
  position fixed
73
73
  top 128px
74
74
  bottom 0
75
- left 43px
75
+ left var(--left-side-bar-width, 43px)
76
76
  right 0
77
77
  overflow-y scroll
78
78
  .setting-row
@@ -7,7 +7,7 @@
7
7
  .setting-wrap .app-drag
8
8
  position fixed
9
9
  top 0
10
- left 43px
10
+ left var(--left-side-bar-width, 43px)
11
11
  height 60px
12
12
  right 0
13
13
  z-index 888
@@ -10,7 +10,6 @@ import { isEqual, last, isNumber, some, isArray, pick, uniq, debounce } from 'lo
10
10
  import FileSection from './file-item'
11
11
  import resolve from '../../common/resolve'
12
12
  import wait from '../../common/wait'
13
- import isAbsPath from '../../common/is-absolute-path'
14
13
  import classnames from 'classnames'
15
14
  import sorterIndex from '../../common/index-sorter'
16
15
  import { handleErr } from '../../common/fetch'
@@ -107,8 +106,6 @@ export default class Sftp extends Component {
107
106
  this.sftp = null
108
107
  clearTimeout(this.timer4)
109
108
  this.timer4 = null
110
- clearTimeout(this.timer5)
111
- this.timer5 = null
112
109
  // Clear sort cache to prevent memory leaks
113
110
  this._sortCache?.clear()
114
111
  this._lastSortArgs = null
@@ -455,9 +452,7 @@ export default class Sftp extends Component {
455
452
  }
456
453
  const type = files[0]?.type || _type
457
454
  const func = this[type + 'Del']
458
- for (const f of files) {
459
- await func(f)
460
- }
455
+ await Promise.all(files.map(f => func(f)))
461
456
  if (type === typeMap.remote) {
462
457
  await wait(500)
463
458
  }
@@ -560,8 +555,10 @@ export default class Sftp extends Component {
560
555
  }
561
556
 
562
557
  initRemoteAll = async () => {
563
- await this.remoteList()
564
- this.remoteListOwner()
558
+ await Promise.all([
559
+ this.remoteList(),
560
+ this.remoteListOwner().catch(e => console.debug('remoteListOwner error:', e))
561
+ ])
565
562
  }
566
563
 
567
564
  modifier = (...args) => {
@@ -647,12 +644,10 @@ export default class Sftp extends Component {
647
644
  }
648
645
 
649
646
  remoteListOwner = async () => {
650
- const remoteUidTree = await owner.remoteListUsers(
651
- this.props.pid
652
- )
653
- const remoteGidTree = await owner.remoteListGroups(
654
- this.props.pid
655
- )
647
+ const [remoteUidTree, remoteGidTree] = await Promise.all([
648
+ owner.remoteListUsers(this.props.pid),
649
+ owner.remoteListGroups(this.props.pid)
650
+ ])
656
651
  this.setState({
657
652
  remoteGidTree,
658
653
  remoteUidTree
@@ -660,8 +655,10 @@ export default class Sftp extends Component {
660
655
  }
661
656
 
662
657
  localListOwner = async () => {
663
- const localUidTree = await owner.localListUsers()
664
- const localGidTree = await owner.localListGroups()
658
+ const [localUidTree, localGidTree] = await Promise.all([
659
+ owner.localListUsers(),
660
+ owner.localListGroups()
661
+ ])
665
662
  this.setState({
666
663
  localGidTree,
667
664
  localUidTree
@@ -800,14 +797,6 @@ export default class Sftp extends Component {
800
797
  sftpCreated: true
801
798
  })
802
799
  })
803
- this.timer5 = setTimeout(() => {
804
- if (this.type !== 'ftp') {
805
- this.updateRemoteList(remote, remotePath, sftp)
806
- }
807
- this.props.editTab(tab.id, {
808
- sftpCreated: true
809
- })
810
- }, 1000)
811
800
  } catch (e) {
812
801
  const update = {
813
802
  remoteLoading: false,
@@ -828,41 +817,34 @@ export default class Sftp extends Component {
828
817
  remotePath,
829
818
  sftp
830
819
  ) => {
831
- const remote = []
832
- for (const r of remotes) {
833
- const { name } = r
834
- if (r.isSymbol) {
835
- const linkPath = resolve(remotePath, name)
836
- let realpath = await sftp.readlink(linkPath)
837
- .catch(e => {
838
- console.debug(e)
839
- return null
840
- })
841
- if (!realpath) {
842
- continue
843
- }
844
- if (!isAbsPath(realpath)) {
845
- realpath = resolve(remotePath, realpath)
846
- realpath = await sftp.realpath(realpath)
820
+ // Resolve all symlinks in parallel for faster updates.
821
+ // Use sftp.stat (which follows symlinks) directly on the link path
822
+ // instead of readlink + realpath + stat, reducing 2-3 SFTP round
823
+ // trips per symlink to just 1.
824
+ const results = await Promise.all(
825
+ remotes.map(async r => {
826
+ if (!r.isSymbol) {
827
+ r.isSymbolicLink = false
828
+ return r
847
829
  }
830
+ const linkPath = resolve(remotePath, r.name)
848
831
  const realFileInfo = await getRemoteFileInfo(
849
832
  sftp,
850
- realpath
833
+ linkPath
851
834
  ).catch(e => {
852
835
  console.debug('seems a bad symbolic link')
853
836
  console.debug(e)
854
837
  return null
855
838
  })
856
839
  if (!realFileInfo) {
857
- continue
840
+ return null
858
841
  }
859
842
  r.isSymbolicLink = true
860
843
  r.isDirectory = realFileInfo.isDirectory
861
- } else {
862
- r.isSymbolicLink = false
863
- }
864
- remote.push(r)
865
- }
844
+ return r
845
+ })
846
+ )
847
+ const remote = results.filter(Boolean)
866
848
  const update = {
867
849
  remote,
868
850
  remoteFileTree: this.buildTree(remote, typeMap.remote)
@@ -892,14 +874,13 @@ export default class Sftp extends Component {
892
874
  this.getCwdLocal() ||
893
875
  this.getLocalHome()
894
876
  const locals = await window.fs.readdirAsync(localPath)
895
- const local = []
896
- for (const name of locals) {
897
- const p = resolve(localPath, name)
898
- const fileObj = await getLocalFileInfo(p).catch(console.log)
899
- if (fileObj) {
900
- local.push(fileObj)
901
- }
902
- }
877
+ const results = await Promise.all(
878
+ locals.map(name => {
879
+ const p = resolve(localPath, name)
880
+ return getLocalFileInfo(p).catch(console.log)
881
+ })
882
+ )
883
+ const local = results.filter(Boolean)
903
884
  const update = {
904
885
  local,
905
886
  inited: true,
@@ -10,7 +10,7 @@ export default auto(function BookmarkSelect (props) {
10
10
  const {
11
11
  listStyle,
12
12
  openedSideBar,
13
- leftSidebarWidth,
13
+ leftSidePanelWidth,
14
14
  expandedKeys,
15
15
  bookmarks,
16
16
  bookmarksMap,
@@ -38,7 +38,7 @@ export default auto(function BookmarkSelect (props) {
38
38
  bookmarksMap,
39
39
  bookmarkGroups: store.getBookmarkGroupsTotal(),
40
40
  expandedKeys,
41
- leftSidebarWidth,
41
+ leftSidePanelWidth,
42
42
  bookmarkGroupTree: store.bookmarkGroupTree,
43
43
  autoFocus,
44
44
  initLoadingData
@@ -8,7 +8,8 @@ import {
8
8
  UpCircleOutlined,
9
9
  AppstoreOutlined,
10
10
  ThunderboltOutlined,
11
- AimOutlined
11
+ AimOutlined,
12
+ MenuFoldOutlined
12
13
  } from '@ant-design/icons'
13
14
  import { Tooltip, Popover } from 'antd'
14
15
  import SideBarPanel from './sidebar-panel'
@@ -16,7 +17,6 @@ import TransferList from './transfer-list'
16
17
  import MenuBtn from '../sys-menu/menu-btn'
17
18
  import QuickConnect from '../tabs/quick-connect'
18
19
  import {
19
- sidebarWidth,
20
20
  settingMap,
21
21
  modals
22
22
  } from '../../common/constants'
@@ -34,7 +34,10 @@ export default function Sidebar (props) {
34
34
  settingTab,
35
35
  settingItem,
36
36
  isSyncingSetting,
37
- leftSidebarWidth,
37
+ // expandable bookmarks/history panel
38
+ leftSidePanelWidth,
39
+ // far-left icon bar (43px; 0 when hidden)
40
+ leftSideBarWidth,
38
41
  pinned,
39
42
  fileTransfers,
40
43
  openedSideBar,
@@ -91,6 +94,10 @@ export default function Sidebar (props) {
91
94
  store.onZoomReset()
92
95
  }
93
96
 
97
+ const handleToggleSidebar = () => {
98
+ store.toggleLeftSideBar()
99
+ }
100
+
94
101
  const {
95
102
  onNewSsh,
96
103
  openSetting,
@@ -115,16 +122,16 @@ export default function Sidebar (props) {
115
122
  ? {
116
123
  className: 'sidebar-list',
117
124
  style: {
118
- width: `${leftSidebarWidth}px`
125
+ width: `${leftSidePanelWidth}px`
119
126
  }
120
127
  }
121
128
  : {
122
129
  className: 'sidebar-list'
123
130
  }
124
131
  const sidebarProps = {
125
- className: `sidebar type-${openedSideBar}`,
132
+ className: `sidebar type-${openedSideBar}${leftSideBarWidth === 0 ? ' collapsed' : ''}`,
126
133
  style: {
127
- width: sidebarWidth,
134
+ width: leftSideBarWidth,
128
135
  height
129
136
  }
130
137
  }
@@ -209,6 +216,14 @@ export default function Sidebar (props) {
209
216
  onClick={openAbout}
210
217
  />
211
218
  </SideIcon>
219
+ <SideIcon
220
+ title={e('hide')}
221
+ >
222
+ <MenuFoldOutlined
223
+ className='iblock font16 control-icon hide-sidebar-icon'
224
+ onClick={handleToggleSidebar}
225
+ />
226
+ </SideIcon>
212
227
  {
213
228
  Math.round((zoom ?? 1) * 100) !== 100
214
229
  ? (
@@ -246,7 +261,8 @@ export default function Sidebar (props) {
246
261
  <SidePanel
247
262
  sideProps={sideProps}
248
263
  setLeftSidePanelWidth={setLeftSidePanelWidth}
249
- leftSidebarWidth={leftSidebarWidth}
264
+ leftSidePanelWidth={leftSidePanelWidth}
265
+ leftSideBarWidth={leftSideBarWidth}
250
266
  >
251
267
  <SideBarPanel
252
268
  pinned={pinned}
@@ -15,13 +15,13 @@ export default function SidePanel (props) {
15
15
  }
16
16
  const el1 = document.querySelector('.sessions')
17
17
  if (el1) {
18
- el1.style.left = (nw + 43) + 'px'
18
+ el1.style.left = (nw + props.leftSideBarWidth) + 'px'
19
19
  }
20
- }, [props.leftSidebarWidth])
20
+ }, [props.leftSidePanelWidth, props.leftSideBarWidth])
21
21
  const dragProps = {
22
22
  min: 343,
23
23
  max: 600,
24
- width: props.leftSidebarWidth,
24
+ width: props.leftSidePanelWidth,
25
25
  onDragEnd,
26
26
  onDragMove,
27
27
  left: true
@@ -93,12 +93,17 @@
93
93
  color var(--text-light)
94
94
  .sidebar-list
95
95
  position absolute
96
- left 43px
96
+ left var(--left-side-bar-width, 43px)
97
97
  top 0
98
98
  bottom 0
99
99
  z-index 200
100
100
  width 0
101
101
 
102
+ // mobile: icon bar hidden — hide its buttons
103
+ .sidebar.collapsed
104
+ .sidebar-bar
105
+ display none
106
+
102
107
  .pinned
103
108
  .sidebar-list
104
109
  top 0
@@ -147,9 +147,17 @@ export async function detectRemoteShell (pid) {
147
147
  // instead of probing for any shell binary installed on the host.
148
148
  const cmd = 'printf "%s\n" "$SHELL"'
149
149
 
150
- const r = await runCmd(pid, cmd)
150
+ // { silent: true } so this best-effort probe does not emit a transport-level
151
+ // fetch warning; a single, clearer warning is logged below if it fails.
152
+ const r = await runCmd(pid, cmd, { silent: true })
151
153
  .catch((err) => {
152
- console.error('detectRemoteShell error', err)
154
+ // Non-fatal: the interactive shell already opened, so the terminal keeps
155
+ // working. We just can't inject OSC 633 shell integration for command
156
+ // tracking. This commonly happens when the server limits concurrent
157
+ // sessions (sshd MaxSessions) or the account uses a forced/restricted
158
+ // command, which rejects the auxiliary exec channel with
159
+ // "(SSH) Channel open failure: open failed".
160
+ console.warn('detectRemoteShell: exec channel rejected by server, shell integration disabled, falling back to sh —', err?.message || err)
153
161
  return 'sh'
154
162
  })
155
163
 
@@ -11,21 +11,21 @@ export function createTerm (body) {
11
11
  })
12
12
  }
13
13
 
14
- export function runCmd (pid, cmd) {
14
+ export function runCmd (pid, cmd, options) {
15
15
  return fetch({
16
16
  pid,
17
17
  cmd,
18
18
  action: 'run-cmd'
19
- })
19
+ }, options)
20
20
  }
21
21
 
22
- export function execCmd (pid, cmd, timeoutMs) {
22
+ export function execCmd (pid, cmd, timeoutMs, options) {
23
23
  return fetch({
24
24
  pid,
25
25
  cmd,
26
26
  timeoutMs,
27
27
  action: 'exec-cmd'
28
- })
28
+ }, options)
29
29
  }
30
30
 
31
31
  export function resizeTerm (pid, cols, rows) {
@@ -73,6 +73,7 @@ export default class TerminalCmdSuggestions extends Component {
73
73
  config.baseURLAI,
74
74
  config.apiPathAI,
75
75
  config.apiKeyAI,
76
+ config.proxyAI,
76
77
  false,
77
78
  config.authHeaderNameAI
78
79
  ).catch(
@@ -166,13 +166,21 @@ class Term extends Component {
166
166
  !isEqual(
167
167
  pick(this.props, names),
168
168
  pick(prevProps, names)
169
- ) ||
170
- shouldChange
169
+ )
171
170
  ) {
172
171
  this.onResize()
173
172
  }
174
173
  if (shouldChange && this.term) {
175
174
  this.term.focus()
175
+ // 标签从 display:none 变为可见时,延迟到下一动画帧再 fit,读取正确布局
176
+ // 尺寸纠正列数;并用 refresh 强制重绘,清除快速连续打开多个连接时隐藏
177
+ // 标签被 0 列 fit 导致的虚假/重复提示符。
178
+ requestAnimationFrame(() => {
179
+ if (this.term && !this.onClose) {
180
+ this.fitAndRefresh()
181
+ setTimeout(() => this.fitAndRefresh(), 80)
182
+ }
183
+ })
176
184
  }
177
185
  this.checkConfigChange(
178
186
  prevProps,
@@ -1530,7 +1538,11 @@ class Term extends Component {
1530
1538
  this.term = term
1531
1539
  term.onSelectionChange(this.onSelectionChange)
1532
1540
  term.attachCustomKeyEventHandler(this.handleKeyboardEvent.bind(this))
1533
- this.fitAddon.fit()
1541
+ // 容器不可见(隐藏标签 display:none)时不 fit,避免算出 0 列把 shell
1542
+ // 提示符逐字符错误换行;待标签激活可见后由 fitAndRefresh 重新适配。
1543
+ if (this.isElementVisible()) {
1544
+ this.fitAddon.fit()
1545
+ }
1534
1546
  await this.remoteInit(term)
1535
1547
  }
1536
1548
 
@@ -1888,7 +1900,10 @@ class Term extends Component {
1888
1900
  this.trzszClient.init(socket)
1889
1901
  this.xmodemClient = new XmodemClient(this)
1890
1902
  this.xmodemClient.init(socket)
1891
- this.fitAddon.fit()
1903
+ // 仅在可见时 fit,隐藏标签跳过,避免 0 列损坏提示符。
1904
+ if (this.isElementVisible()) {
1905
+ this.fitAddon.fit()
1906
+ }
1892
1907
  term.displayRaw = displayRaw
1893
1908
  term.loadAddon(
1894
1909
  new KeywordHighlighterAddon(keywords)
@@ -1952,20 +1967,38 @@ class Term extends Component {
1952
1967
  })
1953
1968
  }
1954
1969
 
1955
- onResize = throttle(() => {
1956
- const cid = this.props.currentBatchTabId
1957
- const tid = this.props.tab?.id
1958
- if (
1959
- this.props.tab.status === statusMap.success &&
1960
- cid === tid &&
1961
- this.term
1962
- ) {
1963
- try {
1964
- this.fitAddon.fit()
1965
- } catch (e) {
1966
- console.info('resize failed')
1967
- }
1970
+ // 判断终端挂载容器当前是否真实可见(非 display:none)。
1971
+ // 隐藏标签的 clientWidth/clientHeight 为 0,此时不应 fit,否则会算出 0 列。
1972
+ isElementVisible = () => {
1973
+ const el = this.domRef.current
1974
+ if (!el) {
1975
+ return false
1968
1976
  }
1977
+ return el.clientWidth > 0 && el.clientHeight > 0
1978
+ }
1979
+
1980
+ // 重新适配终端尺寸并强制重绘可见区域。
1981
+ // 仅当容器真正可见时才 fit:隐藏标签(display:none)下 clientWidth 为 0,
1982
+ // 若在此时 fit 会算出 0/极小列数,使 shell 提示符被逐字符错误换行,
1983
+ // 出现快速连续打开多个连接时的虚假/重复提示符。因此隐藏态跳过 fit,
1984
+ // 保留 xterm 默认 80x24,待标签激活可见后再 fit 重绘,从根上避免换行损坏。
1985
+ fitAndRefresh = () => {
1986
+ if (!this.term || !this.fitAddon || this.onClose) {
1987
+ return
1988
+ }
1989
+ if (!this.isElementVisible()) {
1990
+ return
1991
+ }
1992
+ try {
1993
+ this.fitAddon.fit()
1994
+ this.term.refresh(0, this.term.rows - 1)
1995
+ } catch (e) {
1996
+ console.info('resize failed', e)
1997
+ }
1998
+ }
1999
+
2000
+ onResize = throttle(() => {
2001
+ this.fitAndRefresh()
1969
2002
  }, 200)
1970
2003
 
1971
2004
  onerrorSocket = err => {
@@ -12,7 +12,7 @@ export default function TreeListRow (props) {
12
12
  activeItemId,
13
13
  searchSelectedRowKey,
14
14
  staticList,
15
- leftSidebarWidth,
15
+ leftSidePanelWidth,
16
16
  handleExpand,
17
17
  handleUnExpand,
18
18
  del,
@@ -43,7 +43,7 @@ export default function TreeListRow (props) {
43
43
  itemColor: item?.color,
44
44
  itemDescription: item?.description,
45
45
  itemLevel: item?.level,
46
- leftSidebarWidth,
46
+ leftSidePanelWidth,
47
47
  staticList,
48
48
  selectedItemId: activeItemId,
49
49
  searchSelected: searchSelectedRowKey === row.key,
@@ -748,7 +748,7 @@ export default class ItemListTree extends Component {
748
748
  activeItemId={this.props.activeItemId}
749
749
  searchSelectedRowKey={this.state.searchSelectedRowKey}
750
750
  staticList={this.props.staticList}
751
- leftSidebarWidth={this.props.leftSidebarWidth}
751
+ leftSidePanelWidth={this.props.leftSidePanelWidth}
752
752
  {...pick(
753
753
  this,
754
754
  [
@@ -15,7 +15,7 @@ import {
15
15
  } from '@ant-design/icons'
16
16
  import message from '../common/message'
17
17
  import Modal from '../common/modal'
18
- import { copy } from '../../common/clipboard'
18
+ import { copy, readClipboard } from '../../common/clipboard'
19
19
  import VncForm from './vnc-form'
20
20
  import RemoteFloatControl from '../common/remote-float-control'
21
21
  import './vnc.styl'
@@ -49,11 +49,16 @@ export default class VncSession extends PureComponent {
49
49
 
50
50
  domRef = createRef()
51
51
 
52
+ // clipboard sync state (local -> remote direction)
53
+ lastClip = null
54
+ vncViewOnly = false
55
+
52
56
  componentDidMount () {
53
57
  this.remoteInit()
54
58
  }
55
59
 
56
60
  componentWillUnmount () {
61
+ this._detachClipboardSync()
57
62
  this.rfb && this.rfb.disconnect()
58
63
  delete this.rfb
59
64
  clearTimeout(this.timer)
@@ -379,6 +384,7 @@ export default class VncSession extends PureComponent {
379
384
  rfb.compressionLevel = compressionLevel
380
385
  rfb.viewOnly = viewOnly
381
386
  rfb.showDotCursor = showDotCursor
387
+ this.vncViewOnly = viewOnly
382
388
  this.rfb = rfb
383
389
  }
384
390
 
@@ -388,12 +394,14 @@ export default class VncSession extends PureComponent {
388
394
  this.setState({
389
395
  loading: false
390
396
  })
397
+ this._attachClipboardSync()
391
398
  }
392
399
 
393
400
  onDesktopsize = (event) => {
394
401
  }
395
402
 
396
403
  onDisconnect = () => {
404
+ this._detachClipboardSync()
397
405
  this.setStatus(statusMap.error)
398
406
  }
399
407
 
@@ -433,7 +441,61 @@ export default class VncSession extends PureComponent {
433
441
  }
434
442
 
435
443
  onClipboard = (event) => {
436
- copy(event.detail.text)
444
+ const text = event?.detail?.text
445
+ if (!text || text === this.lastClip) {
446
+ return
447
+ }
448
+ // Track so our click-driven sync won't echo this same text back to the
449
+ // server (some servers re-broadcast what they receive).
450
+ this.lastClip = text
451
+ copy(text)
452
+ }
453
+
454
+ // Push the current local clipboard to the remote when it has changed.
455
+ // This implements the local -> remote direction that noVNC cannot do itself.
456
+ // Triggered on view click instead of polling, so it only runs right when the
457
+ // user is about to interact with the remote desktop (e.g. before pasting).
458
+ handleViewMouseDown = () => {
459
+ const rfb = this.rfb
460
+ if (!rfb || this.vncViewOnly) {
461
+ return
462
+ }
463
+ let text
464
+ try {
465
+ text = readClipboard()
466
+ } catch (e) {
467
+ return
468
+ }
469
+ if (!text || text === this.lastClip) {
470
+ return
471
+ }
472
+ this.lastClip = text
473
+ try {
474
+ rfb.clipboardPasteFrom(text)
475
+ } catch (e) {
476
+ // clipboardPasteFrom can throw if the socket is in a bad state; ignore
477
+ }
478
+ }
479
+
480
+ // noVNC calls stopPropagation() on canvas mouse events in the bubble phase,
481
+ // so React's delegated onClick (or any bubble-phase listener) never fires.
482
+ // We attach a CAPTURE-phase mousedown listener on the wrapper so it runs
483
+ // before noVNC can stop propagation. mousedown also fires earlier than click,
484
+ // giving the remote clipboard time to update before the user pastes.
485
+ _attachClipboardSync = () => {
486
+ this._detachClipboardSync()
487
+ const el = this.domRef.current
488
+ if (!el) {
489
+ return
490
+ }
491
+ el.addEventListener('mousedown', this.handleViewMouseDown, true)
492
+ }
493
+
494
+ _detachClipboardSync = () => {
495
+ const el = this.domRef.current
496
+ if (el) {
497
+ el.removeEventListener('mousedown', this.handleViewMouseDown, true)
498
+ }
437
499
  }
438
500
 
439
501
  onBell = (event) => {
@@ -542,6 +604,7 @@ export default class VncSession extends PureComponent {
542
604
  }
543
605
 
544
606
  handleReInit = () => {
607
+ this._detachClipboardSync()
545
608
  this.rfb?.disconnect()
546
609
  delete this.rfb
547
610
  this.remoteInit()
@@ -93,7 +93,8 @@
93
93
  // ---- 3. Footer encode select: show icon, click to open dropdown
94
94
  // match antd Button small icon size (~14px) used by sibling icons
95
95
  .mobile-encode-trigger,
96
- .terminal-info-icon
96
+ .terminal-info-icon,
97
+ .show-sidebar-icon
97
98
  font-size 14px !important
98
99
  line-height 36px
99
100
  padding 0 4px
@@ -7,7 +7,8 @@ import Modal from '../components/common/modal'
7
7
  import { appendMandatoryGuardrails } from '../components/ai/ai-guardrails'
8
8
  import { debounce, some, get, pickBy } from 'lodash-es'
9
9
  import {
10
- leftSidebarWidthKey,
10
+ leftSidePanelWidthKey,
11
+ leftSideBarOpenKey,
11
12
  rightSidebarWidthKey,
12
13
  addPanelWidthLsKey,
13
14
  dismissDelKeyTipLsKey,
@@ -106,8 +107,19 @@ export default Store => {
106
107
  }
107
108
 
108
109
  Store.prototype.setLeftSidePanelWidth = function (v) {
109
- ls.setItem(leftSidebarWidthKey, v)
110
- window.store._leftSidebarWidth = v
110
+ ls.setItem(leftSidePanelWidthKey, v)
111
+ window.store._leftSidePanelWidth = v
112
+ }
113
+
114
+ Store.prototype.toggleLeftSideBar = function () {
115
+ const { store } = window
116
+ const willOpen = !store._leftSideBarOpen
117
+ store._leftSideBarOpen = willOpen
118
+ ls.setItem(leftSideBarOpenKey, willOpen ? 'true' : 'false')
119
+ if (!willOpen) {
120
+ // hiding the bar also closes/unpins the side panel so no space is reserved
121
+ store.handleCloseSidebar()
122
+ }
111
123
  }
112
124
 
113
125
  Store.prototype.setAddPanelWidth = function (v) {
@@ -9,13 +9,14 @@ import {
9
9
  infoTabs,
10
10
  openedSidebarKey,
11
11
  sidebarPinnedKey,
12
+ leftSideBarOpenKey,
12
13
  pinnedQuickCommandBarKey,
13
14
  sftpDefaultSortSettingKey,
14
15
  batchInputLsKey,
15
16
  expandedKeysLsKey,
16
17
  checkedKeysLsKey,
17
18
  localAddrBookmarkLsKey,
18
- leftSidebarWidthKey,
19
+ leftSidePanelWidthKey,
19
20
  rightSidebarWidthKey,
20
21
  addPanelWidthLsKey,
21
22
  dismissDelKeyTipLsKey,
@@ -46,6 +47,10 @@ function getDefaultBookmarkGroups (bookmarks) {
46
47
 
47
48
  export default () => {
48
49
  const layout = ls.getItem('layout') || splitMap.c1
50
+ // far-left icon bar is open by default; once the user toggles it, honor the
51
+ // saved preference ('true'/'false') from localStorage.
52
+ const storedSideBarOpen = ls.getItem(leftSideBarOpenKey)
53
+ const leftSideBarOpen = storedSideBarOpen === '' ? true : storedSideBarOpen === 'true'
49
54
  return {
50
55
  // common
51
56
  wsInited: false,
@@ -164,7 +169,9 @@ export default () => {
164
169
 
165
170
  // sidebar
166
171
  openedSideBar: ls.getItem(openedSidebarKey) || '',
167
- _leftSidebarWidth: parseInt(ls.getItem(leftSidebarWidthKey), 10) || 300,
172
+ _leftSidePanelWidth: parseInt(ls.getItem(leftSidePanelWidthKey), 10) || 300,
173
+ // whether the far-left icon bar is open (hide/show works on every platform)
174
+ _leftSideBarOpen: leftSideBarOpen,
168
175
  addPanelWidth: parseInt(ls.getItem(addPanelWidthLsKey), 10) || 300,
169
176
  menuOpened: false,
170
177
  pinned: ls.getItem(sidebarPinnedKey) === 'true',
@@ -59,12 +59,21 @@ class Store {
59
59
  return window.store.innerWidth
60
60
  }
61
61
 
62
- get leftSidebarWidth () {
62
+ get leftSidePanelWidth () {
63
63
  const { store } = window
64
64
  if (store.isMobile) {
65
- return Math.min(store.innerWidth - sidebarWidth, 500)
65
+ return Math.min(store.innerWidth - store.leftSideBarWidth, 500)
66
66
  }
67
- return store._leftSidebarWidth
67
+ return store._leftSidePanelWidth
68
+ }
69
+
70
+ // width of the far-left icon bar (the ~43px vertical strip with the menu
71
+ // / bookmarks / settings icons). Distinct from leftSidePanelWidth, which is
72
+ // the expandable bookmarks/history panel. Controlled by _leftSideBarOpen on
73
+ // every platform: sidebarWidth when open, 0 when hidden.
74
+ get leftSideBarWidth () {
75
+ const { store } = window
76
+ return store._leftSideBarOpen ? sidebarWidth : 0
68
77
  }
69
78
 
70
79
  get rightPanelWidth () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.15.198",
3
+ "version": "5.0.7",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",