@electerm/electerm-react 5.0.6 → 5.0.8

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 (38) 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/common/logo-elem.jsx +1 -1
  5. package/client/components/common/logo.styl +8 -2
  6. package/client/components/footer/cmd-history.jsx +13 -7
  7. package/client/components/footer/cmd-history.styl +8 -0
  8. package/client/components/footer/footer-entry.jsx +24 -4
  9. package/client/components/footer/footer.styl +1 -1
  10. package/client/components/layout/layout.jsx +13 -8
  11. package/client/components/main/main.jsx +6 -2
  12. package/client/components/quick-commands/qm.styl +1 -1
  13. package/client/components/quick-commands/quick-commands-box.jsx +5 -21
  14. package/client/components/quick-commands/quick-commands-select.jsx +10 -20
  15. package/client/components/session/session.jsx +1 -1
  16. package/client/components/session/sessions.jsx +1 -1
  17. package/client/components/setting-panel/setting-modal.jsx +1 -1
  18. package/client/components/setting-panel/setting-wrap.jsx +1 -2
  19. package/client/components/setting-panel/setting-wrap.styl +1 -1
  20. package/client/components/setting-panel/setting.styl +1 -1
  21. package/client/components/sidebar/bookmark-select.jsx +2 -2
  22. package/client/components/sidebar/history.jsx +13 -10
  23. package/client/components/sidebar/index.jsx +23 -7
  24. package/client/components/sidebar/side-panel.jsx +3 -3
  25. package/client/components/sidebar/sidebar.styl +12 -1
  26. package/client/components/tabs/no-session.jsx +1 -1
  27. package/client/components/tabs/no-session.styl +31 -8
  28. package/client/components/terminal/shell.js +10 -2
  29. package/client/components/terminal/terminal-apis.js +4 -4
  30. package/client/components/terminal/terminal-command-dropdown.jsx +1 -0
  31. package/client/components/terminal/terminal.jsx +50 -17
  32. package/client/components/tree-list/tree-list-row.jsx +2 -2
  33. package/client/components/tree-list/tree-list.jsx +1 -1
  34. package/client/css/mobile.styl +20 -7
  35. package/client/store/common.js +15 -3
  36. package/client/store/init-state.js +9 -2
  37. package/client/store/store.js +12 -3
  38. 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
 
@@ -9,7 +9,7 @@ import './logo.styl'
9
9
 
10
10
  export default function LogoElem () {
11
11
  return (
12
- <h1 className='mg3y font50'>
12
+ <h1 className='logo-elem mg3y font50'>
13
13
  <span className='iblock pd3 morph-shape mg1l mg1r'>
14
14
  <img src={logoPath2} className='iblock mwm-100 mg1l mg1r logo-filter logo-img' />
15
15
  </span>
@@ -3,8 +3,14 @@
3
3
  .logo-img-small
4
4
  height 28px
5
5
  border-radius 28px
6
- @media (max-width: 500px)
6
+ // the h1 vertical margin so the logo stops dominating the no-session panel.
7
+ .is-mobile
8
+ .logo-elem
9
+ margin-top 8px
10
+ margin-bottom 8px
11
+ .morph-shape
12
+ padding 6px
7
13
  .logo-img
8
- height 40px
14
+ height 32px
9
15
  .logo-img-small
10
16
  height 20px
@@ -111,13 +111,19 @@ export default auto(function CmdHistory (props) {
111
111
  }
112
112
  return (
113
113
  <div className='cmd-history-header pd2b'>
114
- <Switch
115
- checkedChildren={e('sortByFrequency')}
116
- unCheckedChildren={e('sortByFrequency')}
117
- checked={sortByFrequency}
118
- onChange={handleSortByFrequencyChange}
119
- size='small'
120
- />
114
+ <div className='cmd-history-sort'>
115
+ <Switch
116
+ checked={sortByFrequency}
117
+ onChange={handleSortByFrequencyChange}
118
+ size='small'
119
+ />
120
+ <span
121
+ className='cmd-history-sort-label pointer'
122
+ onClick={() => handleSortByFrequencyChange(!sortByFrequency)}
123
+ >
124
+ {e('sortByFrequency')}
125
+ </span>
126
+ </div>
121
127
  <UnorderedListOutlined
122
128
  className='cmd-history-clear-icon pointer clear-ai-icon icon-hover'
123
129
  title={e('clear')}
@@ -17,6 +17,14 @@
17
17
  justify-content space-between
18
18
  padding-bottom 8px
19
19
 
20
+ .cmd-history-sort
21
+ display flex
22
+ align-items center
23
+ gap 6px
24
+
25
+ .cmd-history-sort-label
26
+ user-select none
27
+
20
28
  .cmd-history-clear-icon
21
29
  font-size 14px
22
30
  padding 4px
@@ -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
@@ -2,7 +2,7 @@
2
2
  * quick commands footer selection wrap
3
3
  */
4
4
 
5
- import { useState, useRef } from 'react'
5
+ import { useState } from 'react'
6
6
  import { quickCommandLabelsLsKey, pinnedQuickCommandBarKey } from '../../common/constants'
7
7
  import { sortBy } from 'lodash-es'
8
8
  import { Button, Input, Select, Space, Flex } from 'antd'
@@ -25,21 +25,6 @@ const { Option } = Select
25
25
  export default function QuickCommandsFooterBox (props) {
26
26
  const [keyword, setKeyword] = useState('')
27
27
  const [label, setLabel] = useState(ls.getItem(quickCommandLabelsLsKey, ''))
28
- const timer = useRef(null)
29
-
30
- function handleMouseLeave () {
31
- timer.current = setTimeout(() => {
32
- toggle(false)
33
- }, 500)
34
- }
35
-
36
- function handleMouseEnter () {
37
- clearTimeout(timer.current)
38
- }
39
-
40
- function toggle (openQuickCommandBar) {
41
- window.store.openQuickCommandBar = openQuickCommandBar
42
- }
43
28
 
44
29
  function handleTogglePinned () {
45
30
  const current = !window.store.pinnedQuickCommandBar
@@ -176,7 +161,8 @@ export default function QuickCommandsFooterBox (props) {
176
161
  pinnedQuickCommandBar,
177
162
  qmSortByFrequency,
178
163
  inActiveTerminal,
179
- leftSidebarWidth,
164
+ leftSidePanelWidth,
165
+ leftSideBarWidth,
180
166
  openedSideBar
181
167
  } = props
182
168
  if ((!openQuickCommandBar && !pinnedQuickCommandBar) || !inActiveTerminal) {
@@ -203,14 +189,12 @@ export default function QuickCommandsFooterBox (props) {
203
189
  : 'text'
204
190
  const cls = classNames('qm-list-wrap')
205
191
  const type = qmSortByFrequency ? 'primary' : 'default'
206
- const w = openedSideBar ? 43 + leftSidebarWidth : 43
192
+ const w = openedSideBar ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
207
193
  const qmProps = {
208
194
  className: 'qm-wrap-tooltip',
209
195
  style: {
210
196
  left: w
211
- },
212
- onMouseLeave: handleMouseLeave,
213
- onMouseEnter: handleMouseEnter
197
+ }
214
198
  }
215
199
  return (
216
200
  <div
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * quick commands footer selection
3
+ *
4
+ * Click to toggle the quick-command bar. Click-based so it works identically
5
+ * on touch (mobile) and mouse (desktop) — the old hover-to-open model was
6
+ * unreliable on Android, where a tap synthesizes mouseleave and cancels the
7
+ * open timer.
3
8
  */
4
9
 
5
10
  import { PureComponent } from 'react'
@@ -7,33 +12,18 @@ import { Button } from 'antd'
7
12
  import './qm.styl'
8
13
 
9
14
  export default class QuickCommandsFooter extends PureComponent {
10
- componentWillUnmount () {
11
- clearTimeout(this.timer)
12
- this.timer = null
13
- }
14
-
15
- handleOpen = () => {
16
- this.timer = setTimeout(this.act, 500)
17
- }
18
-
19
- act = () => {
20
- window.store.openQuickCommandBar = true
21
- }
22
-
23
- handleMouseLeave = () => {
24
- clearTimeout(this.timer)
15
+ handleClick = () => {
16
+ const { store } = window
17
+ store.openQuickCommandBar = !store.openQuickCommandBar
25
18
  }
26
19
 
27
20
  render () {
28
21
  return (
29
- <div
30
- className='fleft relative quick-command-trigger-wrap'
31
- onMouseEnter={this.handleOpen}
32
- onMouseLeave={this.handleMouseLeave}
33
- >
22
+ <div className='fleft relative quick-command-trigger-wrap'>
34
23
  <Button
35
24
  size='small'
36
25
  type='text'
26
+ onClick={this.handleClick}
37
27
  >
38
28
  Q
39
29
  </Button>
@@ -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,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
@@ -44,22 +44,25 @@ export default auto(function HistoryPanel (props) {
44
44
  }
45
45
  return (
46
46
  <div className='history-header pd2x pd2b'>
47
- <Switch
48
- {...switchProps}
49
- />
47
+ <div className='history-sort'>
48
+ <Switch
49
+ checked={sortByFrequency}
50
+ onChange={handleSortByFrequencyChange}
51
+ size='small'
52
+ />
53
+ <span
54
+ className='history-sort-label pointer'
55
+ onClick={() => handleSortByFrequencyChange(!sortByFrequency)}
56
+ >
57
+ {e('sortByFrequency')}
58
+ </span>
59
+ </div>
50
60
  <UnorderedListOutlined
51
61
  {...clearIconProps}
52
62
  />
53
63
  </div>
54
64
  )
55
65
  }
56
- const switchProps = {
57
- checkedChildren: e('sortByFrequency'),
58
- unCheckedChildren: e('sortByFrequency'),
59
- checked: sortByFrequency,
60
- onChange: handleSortByFrequencyChange,
61
- size: 'small'
62
- }
63
66
  const clearIconProps = {
64
67
  className: 'history-clear-icon pointer clear-ai-icon icon-hover',
65
68
  title: window.translate('clear'),
@@ -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
@@ -57,6 +57,12 @@
57
57
  top 0
58
58
  z-index 10
59
59
  background var(--main)
60
+ .history-sort
61
+ display flex
62
+ align-items center
63
+ gap 6px
64
+ .history-sort-label
65
+ user-select none
60
66
  .history-clear-icon
61
67
  margin-left auto
62
68
  margin-right 0
@@ -93,12 +99,17 @@
93
99
  color var(--text-light)
94
100
  .sidebar-list
95
101
  position absolute
96
- left 43px
102
+ left var(--left-side-bar-width, 43px)
97
103
  top 0
98
104
  bottom 0
99
105
  z-index 200
100
106
  width 0
101
107
 
108
+ // mobile: icon bar hidden — hide its buttons
109
+ .sidebar.collapsed
110
+ .sidebar-bar
111
+ display none
112
+
102
113
  .pinned
103
114
  .sidebar-list
104
115
  top 0
@@ -51,7 +51,7 @@ export default function NoSessionPanel ({ height, onNewTab, onNewSsh, batch }) {
51
51
  )}
52
52
  <QuickConnect batch={batch} />
53
53
  </div>
54
- <div className='pd3'>
54
+ <div className='no-session-logo'>
55
55
  <LogoElem />
56
56
  </div>
57
57
  <div className='no-session-history' onClick={handleClick}>
@@ -1,28 +1,51 @@
1
1
  .no-sessions
2
2
  background var(--main)
3
3
  text-align center
4
- padding 50px 0
4
+ // stack buttons / logo / history; the history flexes to fill the remaining
5
+ // height, so the layout adapts to logo size and button wrapping without any
6
+ // fixed pixel offsets.
7
+ display flex
8
+ flex-direction column
5
9
  position absolute
6
10
  top 36px
7
11
  left 0
8
12
  right 0
9
13
  bottom 0
14
+ padding 50px 0
10
15
  z-index 11
11
16
  .no-session-btns
17
+ flex-shrink 0
12
18
  display flex
13
19
  flex-wrap wrap
14
20
  justify-content center
15
21
  align-items center
16
22
  gap 8px
17
23
  padding-bottom 8px
24
+ .no-session-logo
25
+ flex-shrink 0
26
+ padding 32px 0
18
27
  .no-session-history
19
- position absolute
20
- top 320px
21
- bottom 80px
22
- left 0
23
- right 0
28
+ flex 1
29
+ min-height 0
30
+ display flex
31
+ flex-direction column
32
+ overflow hidden
24
33
  .sidebar-panel-history
25
- overflow-y auto
34
+ // override the global sidebar absolute positioning (top:112px) so the
35
+ // history fills this flex item instead.
36
+ position static
37
+ flex 1
38
+ min-height 0
26
39
  .pd2x
27
40
  width 400px
28
- margin 0 auto
41
+ margin 0 auto
42
+ .is-mobile
43
+ .no-sessions
44
+ padding 20px 0
45
+ .no-session-logo
46
+ padding 8px 0
47
+ .no-session-history
48
+ .sidebar-panel-history
49
+ .pd2x
50
+ width auto
51
+ max-width 400px
@@ -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
  [
@@ -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
@@ -177,12 +178,21 @@
177
178
 
178
179
  // ---------------------------------------------------------------------------
179
180
  // Touch-primary devices only (feature-detected via pointer media query, not
180
- // UA sniffing or viewport width). iOS Safari and Android Chrome auto-zoom the
181
- // page when focusing an input with font-size < 16px, and never zoom back out
182
- // on blur, leaving the page stuck zoomed. Desktop browsers (even with
183
- // touchscreens or narrow windows) report pointer: fine and never force-zoom,
184
- // so this never matches them. Force 16px on all focusable form fields (antd
185
- // inputs default to 14px) to prevent the zoom.
181
+ // UA sniffing or viewport width). Desktop browsers (even with touchscreens or
182
+ // narrow windows) report pointer: fine, so this never matches them.
183
+ //
184
+ // 1. iOS Safari and Android Chrome auto-zoom the page when focusing an input
185
+ // with font-size < 16px, and never zoom back out on blur, leaving the page
186
+ // stuck zoomed. Force 16px on all focusable form fields (antd inputs default
187
+ // to 14px) to prevent the zoom.
188
+ //
189
+ // 2. Mobile browsers also inflate text to improve readability
190
+ // (-webkit-text-size-adjust defaults to auto). Inside a fixed-size container
191
+ // this makes the text taller than its wrapper, so it gets clipped — the
192
+ // clearest victim is the antd small Switch, whose track is only 16px tall /
193
+ // 28px wide with overflow:hidden; the "sort by frequency" label gets blown
194
+ // past the track and chopped. Pin to 100% so the declared font-size is
195
+ // honored.
186
196
  // ---------------------------------------------------------------------------
187
197
  @media (pointer: coarse)
188
198
  input,
@@ -193,3 +203,6 @@
193
203
  .ant-input-number-input,
194
204
  .ant-select-selection-search-input
195
205
  font-size 16px !important
206
+ html
207
+ -webkit-text-size-adjust 100%
208
+ text-size-adjust 100%
@@ -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": "5.0.6",
3
+ "version": "5.0.8",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",