@electerm/electerm-react 5.3.15 → 5.5.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 (149) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +23 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +14 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/bookmark-form/index.jsx +10 -8
  29. package/client/components/common/item-filter.jsx +76 -0
  30. package/client/components/common/item-filter.styl +54 -0
  31. package/client/components/common/lazy-boundary.jsx +34 -0
  32. package/client/components/common/opacity.jsx +3 -1
  33. package/client/components/footer/footer-entry.jsx +35 -5
  34. package/client/components/footer/footer.styl +8 -1
  35. package/client/components/layout/layout.jsx +23 -31
  36. package/client/components/layout/pixed.js +3 -0
  37. package/client/components/main/main.jsx +80 -28
  38. package/client/components/main/term-fullscreen.styl +9 -3
  39. package/client/components/quick-commands/qm.styl +3 -2
  40. package/client/components/rdp/file-transfer.js +4 -4
  41. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  42. package/client/components/remote-monitor/monitor-details.styl +87 -0
  43. package/client/components/remote-monitor/monitor-model.js +764 -0
  44. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  45. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  46. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  47. package/client/components/remote-monitor/session-monitor.js +361 -0
  48. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  49. package/client/components/remote-monitor/visibility.js +18 -0
  50. package/client/components/session/session-control.jsx +29 -2
  51. package/client/components/session/session.jsx +40 -18
  52. package/client/components/session/sessions.jsx +5 -2
  53. package/client/components/setting-panel/col.jsx +32 -6
  54. package/client/components/setting-panel/list.styl +12 -0
  55. package/client/components/setting-panel/setting-common.jsx +25 -56
  56. package/client/components/setting-panel/setting-mobile-breadcrumb.jsx +29 -0
  57. package/client/components/setting-panel/setting-modal.jsx +64 -44
  58. package/client/components/setting-panel/setting-terminal.jsx +7 -2
  59. package/client/components/setting-panel/setting-wrap.jsx +0 -3
  60. package/client/components/setting-panel/setting-wrap.styl +80 -11
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  67. package/client/components/sidebar/index.jsx +28 -21
  68. package/client/components/sidebar/left-sidebar-icons.jsx +1 -1
  69. package/client/components/sidebar/sidebar.styl +16 -0
  70. package/client/components/sys-menu/menu-btn.jsx +30 -1
  71. package/client/components/tabs/index.jsx +2 -2
  72. package/client/components/tabs/tab-title.jsx +3 -2
  73. package/client/components/tabs/tab.jsx +5 -1
  74. package/client/components/tabs/tabs.styl +4 -0
  75. package/client/components/terminal/attach-addon-custom.js +295 -49
  76. package/client/components/terminal/automation/index.js +64 -0
  77. package/client/components/terminal/automation/keys.js +49 -0
  78. package/client/components/terminal/automation/stream-matcher.js +82 -0
  79. package/client/components/terminal/automation/strip-ansi.js +19 -0
  80. package/client/components/terminal/automation/trigger-engine.js +233 -0
  81. package/client/components/terminal/automation/trigger-presets.js +110 -0
  82. package/client/components/terminal/command-tracker-addon.js +31 -1
  83. package/client/components/terminal/mixins/index.js +27 -0
  84. package/client/components/terminal/mixins/term-attach.js +123 -0
  85. package/client/components/terminal/mixins/term-config.jsx +84 -0
  86. package/client/components/terminal/mixins/term-content.js +88 -0
  87. package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
  88. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  89. package/client/components/terminal/mixins/term-init.js +122 -0
  90. package/client/components/terminal/mixins/term-log.jsx +96 -0
  91. package/client/components/terminal/mixins/term-resize.js +87 -0
  92. package/client/components/terminal/mixins/term-socket.js +433 -0
  93. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  94. package/client/components/terminal/mixins/term-theme.js +190 -0
  95. package/client/components/terminal/mixins/term-touch.js +136 -0
  96. package/client/components/terminal/shell-detect.js +37 -0
  97. package/client/components/terminal/shell.js +0 -28
  98. package/client/components/terminal/shortcut-bar-entry.jsx +32 -27
  99. package/client/components/terminal/shortcut-bar.jsx +4 -6
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +56 -0
  102. package/client/components/terminal/startup-queue.js +379 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal.jsx +102 -2007
  105. package/client/components/terminal/transfer-client-base.js +5 -29
  106. package/client/components/terminal/trzsz-client.js +1 -3
  107. package/client/components/terminal/xmodem-client.js +1 -3
  108. package/client/components/terminal/xterm-loader.js +7 -0
  109. package/client/components/terminal/zmodem-client.js +1 -3
  110. package/client/components/terminal-info/base.jsx +18 -70
  111. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  112. package/client/components/terminal-info/terminal-info.jsx +50 -59
  113. package/client/components/terminal-info/terminal-info.styl +6 -49
  114. package/client/components/text-editor/open-text-editor.js +30 -0
  115. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  116. package/client/components/text-editor/text-editor.jsx +14 -0
  117. package/client/components/theme/theme-form.jsx +22 -0
  118. package/client/components/tree-list/bookmark-transport.jsx +7 -31
  119. package/client/components/tree-list/tree-list.styl +13 -0
  120. package/client/components/triggers/trigger-editor.jsx +246 -0
  121. package/client/components/triggers/trigger-form.jsx +200 -0
  122. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  123. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  124. package/client/components/triggers/trigger-transport.jsx +60 -0
  125. package/client/components/triggers/triggers-field.jsx +19 -0
  126. package/client/components/widgets/widgets-list.jsx +11 -1
  127. package/client/css/mobile.styl +9 -1
  128. package/client/store/common.js +12 -1
  129. package/client/store/init-state.js +14 -0
  130. package/client/store/mcp-handler.js +158 -30
  131. package/client/store/setting.js +20 -0
  132. package/client/store/sidebar.js +12 -0
  133. package/client/store/store.js +2 -0
  134. package/client/store/sync.js +45 -9
  135. package/client/store/system-menu.js +2 -0
  136. package/client/store/tab.js +41 -3
  137. package/client/store/trigger.js +118 -0
  138. package/client/store/watch.js +13 -5
  139. package/client/views/index.pug +3 -0
  140. package/package.json +1 -1
  141. package/client/components/terminal-info/activity.jsx +0 -57
  142. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  143. package/client/components/terminal-info/disk.jsx +0 -45
  144. package/client/components/terminal-info/format-disks.js +0 -45
  145. package/client/components/terminal-info/network.jsx +0 -121
  146. package/client/components/terminal-info/resource.jsx +0 -99
  147. package/client/components/terminal-info/run-cmd.jsx +0 -263
  148. package/client/components/terminal-info/sys-info.jsx +0 -56
  149. package/client/components/terminal-info/up.jsx +0 -17
@@ -1,25 +1,27 @@
1
1
  import { auto } from 'manate/react'
2
2
  import {
3
3
  Select,
4
- Dropdown
4
+ Dropdown,
5
+ Badge
5
6
  } from 'antd'
6
- import { InfoCircleOutlined, TranslationOutlined, DoubleRightOutlined } from '@ant-design/icons'
7
+ import { BarChartOutlined, TranslationOutlined, DoubleRightOutlined, FunctionOutlined } from '@ant-design/icons'
7
8
  import './footer.styl'
8
9
  import { statusMap } from '../../common/constants'
9
10
  import BatchInput from './batch-input'
10
11
  import encodes from '../bookmark-form/common/encodes'
11
12
  import { refs } from '../common/ref'
12
13
  import Qm from '../quick-commands/quick-commands-select'
14
+ import TriggerSessionModal from '../triggers/trigger-session-modal'
13
15
  import AIIcon from '../icons/ai-icon'
14
16
  import { isAIDisabled } from '../../common/ai-feature'
15
17
  import CmdHistory from './cmd-history'
16
18
 
19
+ const e = window.translate
20
+
17
21
  const {
18
22
  Option
19
23
  } = Select
20
24
 
21
- const e = window.translate
22
-
23
25
  export default auto(function FooterEntry (props) {
24
26
  function handleInfoPanel () {
25
27
  window.store.openInfoPanel()
@@ -77,6 +79,33 @@ export default auto(function FooterEntry (props) {
77
79
  )
78
80
  }
79
81
 
82
+ function renderTriggers () {
83
+ const { store } = props
84
+ const tab = store.currentTab
85
+ let count = 0
86
+ try {
87
+ count = tab ? store.getEffectiveTriggers(tab).length : store.triggers.length
88
+ } catch (err) {
89
+ count = 0
90
+ }
91
+ return (
92
+ <div className='terminal-footer-unit terminal-footer-triggers'>
93
+ <Badge
94
+ count={count}
95
+ size='small'
96
+ offset={[-2, 2]}
97
+ >
98
+ <FunctionOutlined
99
+ onClick={() => store.toggleTriggerSessionModal(true)}
100
+ className='pointer font14 terminal-trigger-icon'
101
+ title={e('triggers')}
102
+ />
103
+ </Badge>
104
+ <TriggerSessionModal store={store} />
105
+ </div>
106
+ )
107
+ }
108
+
80
109
  function renderAIIcon () {
81
110
  return (
82
111
  <div className='terminal-footer-unit terminal-footer-ai'>
@@ -149,7 +178,7 @@ export default auto(function FooterEntry (props) {
149
178
  }
150
179
  return (
151
180
  <div className='terminal-footer-unit terminal-footer-info'>
152
- <InfoCircleOutlined
181
+ <BarChartOutlined
153
182
  onClick={handleInfoPanel}
154
183
  className='pointer font14 terminal-info-icon'
155
184
  />
@@ -214,6 +243,7 @@ export default auto(function FooterEntry (props) {
214
243
  {!isAIDisabled() && renderAIIcon()}
215
244
  {renderCmdHistory()}
216
245
  {renderQuickCommands()}
246
+ {renderTriggers()}
217
247
  {renderBatchInputs()}
218
248
  {renderEncodingInfo()}
219
249
  {renderInfoIcon()}
@@ -38,6 +38,9 @@
38
38
  // Q is now an icon-only button (5px internal padding); reduce left
39
39
  // margin so the history icon stays roughly equidistant from AI and Q
40
40
  margin-left 3px
41
+ .terminal-footer-triggers
42
+ display flex
43
+ align-items center
41
44
  .batch-input-outer
42
45
  position relative
43
46
  .bi-full
@@ -61,4 +64,8 @@
61
64
  border-radius 4px 4px 0 0
62
65
  min-width 360px
63
66
  .batch-input-holder
64
- width 100px
67
+ width 100px
68
+ .remote-monitor-bar-on
69
+ .batch-input-outer.bi-show
70
+ .bi-full
71
+ bottom calc(100% + 28px)
@@ -5,12 +5,15 @@ import {
5
5
  splitConfig,
6
6
  quickCommandBoxHeight,
7
7
  footerHeight,
8
+ remoteMonitorBarHeight,
8
9
  shortcutBarHeight
9
10
  } from '../../common/constants'
10
11
  import layoutAlg from './layout-alg'
11
12
  import calcSessionSize from './session-size-alg'
12
13
  import TermSearch from '../terminal/term-search'
13
14
  import Footer from '../footer/footer-entry'
15
+ import RemoteMonitorBar from '../remote-monitor/remote-monitor-bar-entry'
16
+ import { isRemoteMonitorBarVisible } from '../remote-monitor/visibility'
14
17
  import SessionsWrap from '../session/sessions'
15
18
  import QuickCommandsFooterBox from '../quick-commands/quick-commands-box'
16
19
  import pixed from './pixed'
@@ -32,11 +35,10 @@ export default auto(function Layout (props) {
32
35
  const {
33
36
  width,
34
37
  height,
38
+ isMobile,
35
39
  pinnedQuickCommandBar,
36
- // tabsHeight,
37
40
  leftSidePanelWidth,
38
41
  leftSideBarWidth,
39
- // infoPanelPinned,
40
42
  pinned,
41
43
  rightPanelVisible,
42
44
  rightPanelPinned,
@@ -46,47 +48,25 @@ export default auto(function Layout (props) {
46
48
  shortcutBarVisible,
47
49
  shortcutBarKbOffset
48
50
  } = props.store
49
- const h = height - footerHeight - (inActiveTerminal && pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0) + resizeTrigger
50
- const l = pinned ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
51
- const r = rightPanelVisible && rightPanelPinned ? rightPanelWidth : 0
51
+ const monitorHeight = isRemoteMonitorBarVisible(props.store) ? remoteMonitorBarHeight : 0
52
+ const h = height - footerHeight - monitorHeight - (inActiveTerminal && pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0) + resizeTrigger
53
+ const l = pinned && !isMobile ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
54
+ const r = rightPanelVisible && rightPanelPinned && !isMobile ? rightPanelWidth : 0
52
55
  return {
53
56
  height: h,
54
57
  top: 0,
55
58
  left: l,
56
- width: width - l - r
59
+ width: Math.max(0, width - l - r)
57
60
  }
58
61
  }
59
62
 
60
- const buildLayoutStyles = () => {
61
- const {
62
- layout,
63
- height,
64
- width,
65
- pinnedQuickCommandBar,
66
- leftSidePanelWidth,
67
- leftSideBarWidth,
68
- rightPanelVisible,
69
- rightPanelPinned,
70
- rightPanelWidth,
71
- pinned,
72
- shortcutBarVisible,
73
- shortcutBarKbOffset
74
- } = props.store
75
- const l = pinned ? leftSidePanelWidth : 0
76
- const r = rightPanelPinned && rightPanelVisible ? rightPanelWidth : 0
77
- // account for the far-left icon bar (sidebarWidth - 1px border on desktop;
78
- // 0 when the bar is hidden on mobile)
79
- const w = width - l - r - (leftSideBarWidth > 0 ? leftSideBarWidth - 1 : 0)
80
- const h = height - footerHeight - (pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0)
81
- return layoutAlg(layout, w, h)
82
- }
83
63
  const layoutSize = calcLayoutStyle()
84
64
  const {
85
65
  width,
86
66
  height
87
67
  } = layoutSize
88
68
  const pixedLayoutStyle = pixed(layoutSize)
89
- const styles = buildLayoutStyles(conf, layout)
69
+ const styles = layoutAlg(layout, width, height)
90
70
  const layoutProps = {
91
71
  layout,
92
72
  ...styles,
@@ -102,7 +82,14 @@ export default auto(function Layout (props) {
102
82
  const { tabs } = store
103
83
  const tabsBatch = {}
104
84
  for (const tab of tabs) {
105
- const { batch } = tab
85
+ let { batch } = tab
86
+ // Guard against tabs with missing/invalid batch (e.g. created by
87
+ // MCP/AI operations before sanitization). Route them to pane 0
88
+ // instead of crashing or disappearing.
89
+ batch = Number(batch)
90
+ if (!Number.isInteger(batch) || batch < 0 || batch >= sizes.length) {
91
+ batch = 0
92
+ }
106
93
  if (!tabsBatch[batch]) {
107
94
  tabsBatch[batch] = []
108
95
  }
@@ -207,6 +194,11 @@ export default auto(function Layout (props) {
207
194
  key='QuickCommandsFooterBox'
208
195
  {...qmProps}
209
196
  />,
197
+ <RemoteMonitorBar
198
+ key='RemoteMonitorBar'
199
+ store={store}
200
+ style={{ left: layoutSize.left, width, height: remoteMonitorBarHeight }}
201
+ />,
210
202
  <Footer
211
203
  key='Footer'
212
204
  {...footerProps}
@@ -1,4 +1,7 @@
1
1
  export default function pixed (style) {
2
+ if (!style || typeof style !== 'object') {
3
+ return {}
4
+ }
2
5
  return Object.keys(style).reduce((prev, k) => {
3
6
  const v = style[k]
4
7
  return {
@@ -1,5 +1,5 @@
1
1
  import { auto } from 'manate/react'
2
- import { useEffect } from 'react'
2
+ import { lazy, Suspense, useEffect } from 'react'
3
3
  import Layout from '../layout/layout'
4
4
  import FileInfoModal from '../sftp/file-info-modal'
5
5
  import FileCompareModal from '../sftp/file-compare-modal'
@@ -10,7 +10,6 @@ import Sidebar from '../sidebar'
10
10
  import CssOverwrite from '../bg/css-overwrite'
11
11
  import UiTheme from './ui-theme'
12
12
  import CustomCss from '../bg/custom-css.jsx'
13
- import Resolutions from '../rdp/resolution-edit'
14
13
  import TerminalInteractive from '../terminal/terminal-interactive'
15
14
  import ConfirmModalStore from '../file-transfer/conflict-resolve.jsx'
16
15
  import TransferQueue from '../file-transfer/transfer-queue'
@@ -19,18 +18,21 @@ import TerminalCmdSuggestions from '../terminal/terminal-command-dropdown'
19
18
  import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
20
19
  import classnames from 'classnames'
21
20
  import ShortcutControl from '../shortcuts/shortcut-control.jsx'
22
- import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
21
+ import {
22
+ footerHeight,
23
+ isMac,
24
+ isWin,
25
+ remoteMonitorBarHeight,
26
+ textTerminalBgValue
27
+ } from '../../common/constants'
23
28
  import { isAIDisabled } from '../../common/ai-feature'
24
29
  import { ConfigProvider } from 'antd'
25
30
  import { NotificationContainer } from '../common/notification'
26
- import InfoModal from '../sidebar/info-modal.jsx'
27
31
  import RightSidePanel from '../side-panel-r/side-panel-r'
28
32
  import ConnectionHoppingWarning from './connection-hopping-warnning'
29
33
  import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
30
34
  import LoadSshConfigs from '../ssh-config/load-ssh-configs'
31
35
  import AIChat from '../ai/ai-chat-entry'
32
- import AIConfigModal from '../ai/ai-config-modal'
33
- import Opacity from '../common/opacity'
34
36
  import MoveItemModal from '../tree-list/move-item-modal'
35
37
  import InputContextMenu from '../common/input-context-menu'
36
38
  import WorkspaceSaveModal from '../tabs/workspace-save-modal'
@@ -44,9 +46,17 @@ import deepCopy from 'json-deep-copy'
44
46
  import './wrapper.styl'
45
47
  import TerminalInfo from '../terminal-info/terminal-info-entry'
46
48
  import ShortcutBarEntry from '../terminal/shortcut-bar-entry'
49
+ import LazyBoundary from '../common/lazy-boundary'
50
+ import { isRemoteMonitorBarVisible } from '../remote-monitor/visibility'
47
51
  import '../../common/fs.js'
48
52
  import './term-fullscreen.styl'
49
53
 
54
+ const Resolutions = lazy(() => import('../rdp/resolution-edit'))
55
+ const InfoModal = lazy(() => import('../sidebar/info-modal.jsx'))
56
+ const AIConfigModal = lazy(() => import('../ai/ai-config-modal'))
57
+ // window opacity is electron-only: lazy so the web app never loads the chunk
58
+ const Opacity = lazy(() => import('../common/opacity'))
59
+
50
60
  export default auto(function Index (props) {
51
61
  useEffect(() => {
52
62
  const { store } = props
@@ -86,6 +96,30 @@ export default auto(function Index (props) {
86
96
  store.checkPendingDeepLink()
87
97
  }, [])
88
98
 
99
+ // Track the actual input modality rather than a static capability probe:
100
+ // a touch-capable laptop is a mouse machine until a real touch happens,
101
+ // and a tablet stays touch even though it can also pair a mouse. Stored in
102
+ // `store.isTouchDevice` (seeded false — no upfront probe, which only says
103
+ // the screen *can* be touched, not what the user operates with); the
104
+ // `is-touch-device` class drives always-visible hover-only action icons, so
105
+ // it must follow the device the user is actually operating.
106
+ useEffect(() => {
107
+ const { store } = window
108
+ const handlePointer = (e) => {
109
+ if (e.pointerType === 'mouse') {
110
+ store.isTouchDevice = false
111
+ } else if (e.pointerType === 'touch' || e.pointerType === 'pen') {
112
+ store.isTouchDevice = true
113
+ }
114
+ }
115
+ document.addEventListener('pointerdown', handlePointer)
116
+ document.addEventListener('pointermove', handlePointer)
117
+ return () => {
118
+ document.removeEventListener('pointerdown', handlePointer)
119
+ document.removeEventListener('pointermove', handlePointer)
120
+ }
121
+ }, [])
122
+
89
123
  const { store } = props
90
124
  const {
91
125
  configLoaded,
@@ -105,6 +139,7 @@ export default auto(function Index (props) {
105
139
  widgetInstances
106
140
  } = store
107
141
  const upgradeInfo = deepCopy(store.upgradeInfo)
142
+ const remoteMonitorBarVisible = isRemoteMonitorBarVisible(store)
108
143
  const cls = classnames({
109
144
  loaded: configLoaded,
110
145
  'not-webapp': !window.et.isWebApp,
@@ -122,12 +157,15 @@ export default auto(function Index (props) {
122
157
  'fs-with-footer': fullscreen && store.inActiveTerminal,
123
158
  'is-main': !isSecondInstance,
124
159
  'is-mobile': store.isMobile,
125
- 'is-desktop': !store.isMobile
160
+ 'is-desktop': !store.isMobile,
161
+ 'is-touch-device': store.isTouchDevice,
162
+ 'remote-monitor-bar-on': remoteMonitorBarVisible
126
163
  })
127
164
  const ext1 = {
128
165
  className: cls,
129
166
  style: {
130
- '--left-side-bar-width': store.leftSideBarWidth + 'px'
167
+ '--left-side-bar-width': store.leftSideBarWidth + 'px',
168
+ '--footer-stack-height': `${footerHeight + (remoteMonitorBarVisible ? remoteMonitorBarHeight : 0)}px`
131
169
  }
132
170
  }
133
171
  // Get active tab IDs
@@ -217,20 +255,6 @@ export default auto(function Index (props) {
217
255
  title: rightPanelTitle,
218
256
  rightPanelTab
219
257
  }
220
- const terminalInfoProps = {
221
- rightPanelTab,
222
- ...deepCopy(store.terminalInfoProps),
223
- ...pick(
224
- config,
225
- [
226
- 'host',
227
- 'port',
228
- 'saveTerminalLogToFile',
229
- 'terminalInfos',
230
- 'sessionLogPath'
231
- ]
232
- )
233
- }
234
258
  const sshConfigProps = {
235
259
  ...pick(store, [
236
260
  'settingTab',
@@ -268,13 +292,23 @@ export default auto(function Index (props) {
268
292
  {...confsCss}
269
293
  configLoaded={configLoaded}
270
294
  />
271
- <Opacity opacity={config.opacity} />
295
+ {window.et.isWebApp
296
+ ? null
297
+ : (
298
+ <LazyBoundary>
299
+ <Suspense fallback={null}>
300
+ <Opacity opacity={config.opacity} />
301
+ </Suspense>
302
+ </LazyBoundary>
303
+ )}
272
304
  <TerminalInteractive />
273
305
  <UiTheme
274
306
  {...themeProps}
275
307
  />
276
308
  <CustomCss customCss={config.customCss} configLoaded={configLoaded} />
277
- <TextEditor />
309
+ {store.textEditorRequested && (
310
+ <TextEditor />
311
+ )}
278
312
  <UpdateCheck
279
313
  skipVersion={config.skipVersion}
280
314
  upgradeInfo={upgradeInfo}
@@ -300,11 +334,23 @@ export default auto(function Index (props) {
300
334
  config={config}
301
335
  />
302
336
  <Remote2RemoteHandlers />
303
- <Resolutions {...resProps} />
304
- <InfoModal {...infoModalProps} />
337
+ {openResolutionEdit && (
338
+ <LazyBoundary>
339
+ <Suspense fallback={null}>
340
+ <Resolutions {...resProps} />
341
+ </Suspense>
342
+ </LazyBoundary>
343
+ )}
344
+ {store.showInfoModal && (
345
+ <LazyBoundary>
346
+ <Suspense fallback={null}>
347
+ <InfoModal {...infoModalProps} />
348
+ </Suspense>
349
+ </LazyBoundary>
350
+ )}
305
351
  <RightSidePanel {...rightPanelProps}>
306
352
  {!isAIDisabled() && <AIChat {...aiChatProps} />}
307
- <TerminalInfo key={store.activeTabId} {...terminalInfoProps} />
353
+ <TerminalInfo key={store.activeTabId} store={store} {...deepCopy(store.terminalInfoProps)} />
308
354
  </RightSidePanel>
309
355
  <SshConfigLoadNotify {...sshConfigProps} />
310
356
  <LoadSshConfigs
@@ -321,7 +367,13 @@ export default auto(function Index (props) {
321
367
  <BatchOpRunner />
322
368
  <ImportProgress />
323
369
  <ShortcutBarEntry store={store} />
324
- {!isAIDisabled() && <AIConfigModal store={store} />}
370
+ {!isAIDisabled() && store.showAIConfigModal && (
371
+ <LazyBoundary>
372
+ <Suspense fallback={null}>
373
+ <AIConfigModal store={store} />
374
+ </Suspense>
375
+ </LazyBoundary>
376
+ )}
325
377
  <UnixTimestampTooltip />
326
378
  </div>
327
379
  </ConfigProvider>
@@ -5,6 +5,7 @@
5
5
  .terminal-control
6
6
  .qm-wrap-tooltip
7
7
  .main-footer
8
+ .remote-monitor-bar
8
9
  .session-v-info
9
10
  display none
10
11
  // Mobile tabs carry the extra .mobile-tabs class, and mobile.styl uses
@@ -40,11 +41,16 @@
40
41
  // visible; the session shrinks to sit on top of the footer. RDP/VNC/Spice
41
42
  // fullscreen stays footer-less — inActiveTerminal is false for those tabs, so
42
43
  // the fs-with-footer class is absent there.
43
- // 36px matches footerHeight / the .main-footer height in footer.styl.
44
+ // --footer-stack-height includes the 36px footer and the optional monitor row.
44
45
  .fullscreen.fs-with-footer
45
46
  .main-footer
46
47
  display block
47
48
  left 0 !important
49
+ &.remote-monitor-bar-on
50
+ .remote-monitor-bar
51
+ display flex
52
+ left 0 !important
53
+ width 100% !important
48
54
  .qm-wrap-tooltip
49
55
  display block
50
56
  left 0 !important
@@ -53,10 +59,10 @@
53
59
  display none
54
60
  .session-wrap.session-current
55
61
  .term-wrap
56
- height calc(100% - 36px - var(--shortcut-bar-h, 0px)) !important
62
+ height calc(100% - var(--footer-stack-height, 36px) - var(--shortcut-bar-h, 0px) - var(--shortcut-bar-kb-offset, 0px)) !important
57
63
  // pinned quick-command bar (180px, same as quickCommandBoxHeight) sits
58
64
  // between terminal and footer — reserve its space too
59
65
  &.qm-pinned
60
66
  .session-wrap.session-current
61
67
  .term-wrap
62
- height calc(100% - 36px - 180px - var(--shortcut-bar-h, 0px)) !important
68
+ height calc(100% - var(--footer-stack-height, 36px) - 180px - var(--shortcut-bar-h, 0px) - var(--shortcut-bar-kb-offset, 0px)) !important
@@ -9,7 +9,8 @@
9
9
  right 0
10
10
  z-index 89
11
11
  height auto
12
- bottom 36px
12
+
13
+ bottom var(--footer-stack-height, 36px)
13
14
 
14
15
  .qm-pinned
15
16
  .qm-wrap-tooltip
@@ -101,4 +102,4 @@
101
102
  > .drag
102
103
  align-self flex-start
103
104
  > button:last-child
104
- align-self flex-end
105
+ align-self flex-end
@@ -7,6 +7,7 @@ import { getLocalFileInfo } from '../sftp/file-read'
7
7
  import { osResolve } from '../../common/resolve'
8
8
  import { filesize } from 'filesize'
9
9
  import sanitizeFilename from '../../common/sanitize-filename'
10
+ import { chooseSaveDirectory } from '../../common/choose-save-folder'
10
11
 
11
12
  const LOG_PREFIX = '[RDP-FILE-TRANSFER]'
12
13
 
@@ -231,23 +232,22 @@ export class FileTransferManager {
231
232
 
232
233
  async handleFileDownload (fileInfo) {
233
234
  try {
234
- const savePath = await window.api.openDialog({
235
+ const savePath = await chooseSaveDirectory({
235
236
  title: `Save ${fileInfo.name}`,
236
237
  message: `Choose where to save ${fileInfo.name}`,
237
- properties: ['openDirectory', 'createDirectory'],
238
238
  noBrowserTransfer: true
239
239
  }).catch((err) => {
240
240
  this.log(`Save dialog error: ${err.message}`, 'error')
241
241
  return false
242
242
  })
243
243
 
244
- if (!savePath || !savePath.length) {
244
+ if (!savePath) {
245
245
  this.log('Download cancelled by user', 'info')
246
246
  return
247
247
  }
248
248
 
249
249
  const safeName = sanitizeFilename(fileInfo.name)
250
- const fullPath = osResolve(savePath[0], safeName)
250
+ const fullPath = osResolve(savePath, safeName)
251
251
 
252
252
  const fd = await new Promise((resolve, reject) => {
253
253
  window.fs.open(fullPath, O_WRONLY | O_CREAT | O_TRUNC, (err, fd) => {