@electerm/electerm-react 5.3.26 → 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 (134) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +20 -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/common/item-filter.jsx +76 -0
  29. package/client/components/common/item-filter.styl +54 -0
  30. package/client/components/common/lazy-boundary.jsx +34 -0
  31. package/client/components/common/opacity.jsx +3 -1
  32. package/client/components/footer/footer-entry.jsx +35 -5
  33. package/client/components/footer/footer.styl +8 -1
  34. package/client/components/layout/layout.jsx +23 -31
  35. package/client/components/layout/pixed.js +3 -0
  36. package/client/components/main/main.jsx +55 -28
  37. package/client/components/main/term-fullscreen.styl +9 -3
  38. package/client/components/quick-commands/qm.styl +3 -2
  39. package/client/components/rdp/file-transfer.js +4 -4
  40. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  41. package/client/components/remote-monitor/monitor-details.styl +87 -0
  42. package/client/components/remote-monitor/monitor-model.js +764 -0
  43. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  44. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  45. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  46. package/client/components/remote-monitor/session-monitor.js +361 -0
  47. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  48. package/client/components/remote-monitor/visibility.js +18 -0
  49. package/client/components/session/session-control.jsx +29 -2
  50. package/client/components/session/session.jsx +40 -18
  51. package/client/components/session/sessions.jsx +5 -2
  52. package/client/components/setting-panel/setting-common.jsx +25 -56
  53. package/client/components/setting-panel/setting-modal.jsx +57 -42
  54. package/client/components/setting-panel/setting-terminal.jsx +7 -2
  55. package/client/components/setting-panel/setting.styl +1 -1
  56. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  57. package/client/components/sftp/file-compare-modal.jsx +6 -3
  58. package/client/components/sftp/file-item.jsx +5 -3
  59. package/client/components/sftp/owner-list.js +11 -2
  60. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  61. package/client/components/sys-menu/menu-btn.jsx +30 -1
  62. package/client/components/tabs/index.jsx +2 -2
  63. package/client/components/tabs/tab-title.jsx +3 -2
  64. package/client/components/tabs/tab.jsx +5 -1
  65. package/client/components/terminal/attach-addon-custom.js +295 -49
  66. package/client/components/terminal/automation/index.js +64 -0
  67. package/client/components/terminal/automation/keys.js +49 -0
  68. package/client/components/terminal/automation/stream-matcher.js +82 -0
  69. package/client/components/terminal/automation/strip-ansi.js +19 -0
  70. package/client/components/terminal/automation/trigger-engine.js +233 -0
  71. package/client/components/terminal/automation/trigger-presets.js +110 -0
  72. package/client/components/terminal/command-tracker-addon.js +31 -1
  73. package/client/components/terminal/mixins/index.js +27 -0
  74. package/client/components/terminal/mixins/term-attach.js +123 -0
  75. package/client/components/terminal/mixins/term-config.jsx +84 -0
  76. package/client/components/terminal/mixins/term-content.js +88 -0
  77. package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
  78. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  79. package/client/components/terminal/mixins/term-init.js +122 -0
  80. package/client/components/terminal/mixins/term-log.jsx +96 -0
  81. package/client/components/terminal/mixins/term-resize.js +87 -0
  82. package/client/components/terminal/mixins/term-socket.js +433 -0
  83. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  84. package/client/components/terminal/mixins/term-theme.js +190 -0
  85. package/client/components/terminal/mixins/term-touch.js +136 -0
  86. package/client/components/terminal/shell-detect.js +37 -0
  87. package/client/components/terminal/shell.js +0 -28
  88. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  89. package/client/components/terminal/shortcut-bar.jsx +4 -6
  90. package/client/components/terminal/shortcut-bar.styl +4 -0
  91. package/client/components/terminal/ssh-reload-state.js +56 -0
  92. package/client/components/terminal/startup-queue.js +379 -0
  93. package/client/components/terminal/terminal-interactive.jsx +32 -0
  94. package/client/components/terminal/terminal.jsx +102 -2007
  95. package/client/components/terminal/transfer-client-base.js +5 -29
  96. package/client/components/terminal/trzsz-client.js +1 -3
  97. package/client/components/terminal/xmodem-client.js +1 -3
  98. package/client/components/terminal/xterm-loader.js +7 -0
  99. package/client/components/terminal/zmodem-client.js +1 -3
  100. package/client/components/terminal-info/base.jsx +18 -70
  101. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  102. package/client/components/terminal-info/terminal-info.jsx +50 -59
  103. package/client/components/terminal-info/terminal-info.styl +6 -49
  104. package/client/components/text-editor/open-text-editor.js +30 -0
  105. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  106. package/client/components/text-editor/text-editor.jsx +14 -0
  107. package/client/components/triggers/trigger-editor.jsx +246 -0
  108. package/client/components/triggers/trigger-form.jsx +200 -0
  109. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  110. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  111. package/client/components/triggers/trigger-transport.jsx +60 -0
  112. package/client/components/triggers/triggers-field.jsx +19 -0
  113. package/client/components/widgets/widgets-list.jsx +11 -1
  114. package/client/css/mobile.styl +9 -1
  115. package/client/store/common.js +12 -1
  116. package/client/store/init-state.js +7 -0
  117. package/client/store/mcp-handler.js +158 -30
  118. package/client/store/setting.js +9 -0
  119. package/client/store/store.js +2 -0
  120. package/client/store/sync.js +2 -0
  121. package/client/store/tab.js +41 -3
  122. package/client/store/trigger.js +118 -0
  123. package/client/store/watch.js +13 -5
  124. package/client/views/index.pug +3 -0
  125. package/package.json +1 -1
  126. package/client/components/terminal-info/activity.jsx +0 -57
  127. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  128. package/client/components/terminal-info/disk.jsx +0 -45
  129. package/client/components/terminal-info/format-disks.js +0 -45
  130. package/client/components/terminal-info/network.jsx +0 -121
  131. package/client/components/terminal-info/resource.jsx +0 -99
  132. package/client/components/terminal-info/run-cmd.jsx +0 -263
  133. package/client/components/terminal-info/sys-info.jsx +0 -56
  134. package/client/components/terminal-info/up.jsx +0 -17
@@ -0,0 +1,361 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import {
3
+ REMOTE_MONITOR_COMMANDS,
4
+ REMOTE_MONITOR_INTERVALS,
5
+ deriveNetworkRates,
6
+ getUsageLevel,
7
+ parseActivities,
8
+ parseCpu,
9
+ parseDisks,
10
+ parseMemory,
11
+ parseNetwork,
12
+ parseSysInfo,
13
+ parseUptime,
14
+ parseUsers
15
+ } from './monitor-model.js'
16
+
17
+ const GROUP_NAMES = Object.keys(REMOTE_MONITOR_COMMANDS)
18
+ const monitors = new Map()
19
+
20
+ async function executeRemoteCommand (sessionId, command) {
21
+ const { execCmd } = await import('../terminal/terminal-apis.js')
22
+ return execCmd(sessionId, command, 5000, { silent: true })
23
+ }
24
+ let subscriptionSequence = 0
25
+
26
+ function emptyGroup () {
27
+ return {
28
+ status: 'idle',
29
+ data: null,
30
+ updatedAt: null,
31
+ error: null
32
+ }
33
+ }
34
+
35
+ export function createEmptyMonitorSnapshot (sessionId = '') {
36
+ return {
37
+ sessionId,
38
+ groups: Object.fromEntries(GROUP_NAMES.map(name => [name, emptyGroup()])),
39
+ cpuHistory: [],
40
+ levels: { cpu: 'unknown', memory: 'unknown', disks: {} }
41
+ }
42
+ }
43
+
44
+ function unsupportedResult (result) {
45
+ if (!result || typeof result !== 'object') {
46
+ return false
47
+ }
48
+ const message = `${result.stderr || ''} ${result.error || ''}`
49
+ return result.exitCode === 126 ||
50
+ result.exitCode === 127 ||
51
+ /not found|unsupported|no such file/i.test(message)
52
+ }
53
+
54
+ function failedResult (result) {
55
+ return !result ||
56
+ typeof result !== 'object' ||
57
+ result.timedOut === true ||
58
+ (result.exitCode !== null && result.exitCode !== undefined && result.exitCode !== 0)
59
+ }
60
+
61
+ function parseGroup (name, results, sampleTimestamp, previousNetwork) {
62
+ if (name === 'sysinfo') {
63
+ return parseSysInfo(results[0], results[1])
64
+ }
65
+ const result = results[0]
66
+ if (name === 'cpu') return parseCpu(result)
67
+ if (name === 'memory') return parseMemory(result)
68
+ if (name === 'uptime') return parseUptime(result, sampleTimestamp)
69
+ if (name === 'users') return parseUsers(result)
70
+ if (name === 'disks') return parseDisks(result)
71
+ if (name === 'activities') return parseActivities(result)
72
+ if (name === 'network') {
73
+ const parsed = parseNetwork(result)
74
+ if (!parsed) {
75
+ return null
76
+ }
77
+ const current = {
78
+ ...parsed,
79
+ sampleTimestamp
80
+ }
81
+ return {
82
+ data: deriveNetworkRates(previousNetwork, current, sampleTimestamp),
83
+ raw: current
84
+ }
85
+ }
86
+ return null
87
+ }
88
+
89
+ export class SessionMonitor {
90
+ constructor (sessionId, executor) {
91
+ this.sessionId = sessionId
92
+ this.executor = executor || (command => executeRemoteCommand(
93
+ this.sessionId,
94
+ command
95
+ ))
96
+ this.snapshot = createEmptyMonitorSnapshot(sessionId)
97
+ this.subscriptions = new Map()
98
+ this.runners = new Map()
99
+ this.networkSample = null
100
+ this.cleanupTimer = null
101
+ }
102
+
103
+ subscribe (groups, listener) {
104
+ const id = ++subscriptionSequence
105
+ clearTimeout(this.cleanupTimer)
106
+ this.cleanupTimer = null
107
+ this.subscriptions.set(id, {
108
+ groups: new Set(groups.filter(name => GROUP_NAMES.includes(name))),
109
+ listener
110
+ })
111
+ listener(this.snapshot)
112
+ this.reconcile()
113
+ return () => {
114
+ this.subscriptions.delete(id)
115
+ this.reconcile()
116
+ if (!this.subscriptions.size) {
117
+ this.cleanupTimer = setTimeout(() => {
118
+ if (!this.subscriptions.size) {
119
+ this.destroy()
120
+ monitors.delete(this.sessionId)
121
+ }
122
+ }, 5 * 60 * 1000)
123
+ }
124
+ }
125
+ }
126
+
127
+ requestedGroups () {
128
+ const requested = new Set()
129
+ for (const subscription of this.subscriptions.values()) {
130
+ for (const group of subscription.groups) {
131
+ requested.add(group)
132
+ }
133
+ }
134
+ return requested
135
+ }
136
+
137
+ reconcile () {
138
+ const requested = this.requestedGroups()
139
+ for (const name of this.runners.keys()) {
140
+ if (!requested.has(name)) {
141
+ this.stopGroup(name)
142
+ }
143
+ }
144
+ for (const name of requested) {
145
+ this.startGroup(name)
146
+ }
147
+ }
148
+
149
+ startGroup (name) {
150
+ if (this.runners.has(name)) {
151
+ return
152
+ }
153
+ const current = this.snapshot.groups[name]
154
+ if (REMOTE_MONITOR_INTERVALS[name] === 0 && current.status === 'ready') {
155
+ return
156
+ }
157
+ if (current.status === 'unsupported') {
158
+ return
159
+ }
160
+ const runner = {
161
+ timer: null,
162
+ failures: 0
163
+ }
164
+ this.runners.set(name, runner)
165
+ this.runGroup(name, runner)
166
+ }
167
+
168
+ stopGroup (name) {
169
+ const runner = this.runners.get(name)
170
+ if (!runner) {
171
+ return
172
+ }
173
+ clearTimeout(runner.timer)
174
+ this.runners.delete(name)
175
+ if (name === 'network') {
176
+ this.networkSample = null
177
+ }
178
+ }
179
+
180
+ updateGroup (name, update) {
181
+ this.snapshot = {
182
+ ...this.snapshot,
183
+ groups: {
184
+ ...this.snapshot.groups,
185
+ [name]: {
186
+ ...this.snapshot.groups[name],
187
+ ...update
188
+ }
189
+ }
190
+ }
191
+ this.emit()
192
+ }
193
+
194
+ emit () {
195
+ for (const subscription of this.subscriptions.values()) {
196
+ subscription.listener(this.snapshot)
197
+ }
198
+ }
199
+
200
+ async execute (command) {
201
+ return this.executor(command)
202
+ }
203
+
204
+ async runGroup (name, runner) {
205
+ if (this.runners.get(name) !== runner) {
206
+ return
207
+ }
208
+ const previous = this.snapshot.groups[name]
209
+ if (previous.data === null || previous.data === undefined) {
210
+ this.updateGroup(name, {
211
+ status: 'loading',
212
+ error: null
213
+ })
214
+ }
215
+ let unsupported = false
216
+ try {
217
+ const commands = Array.isArray(REMOTE_MONITOR_COMMANDS[name])
218
+ ? REMOTE_MONITOR_COMMANDS[name]
219
+ : [REMOTE_MONITOR_COMMANDS[name]]
220
+ const results = []
221
+ for (const command of commands) {
222
+ const result = await this.execute(command)
223
+ if (this.runners.get(name) !== runner) {
224
+ return
225
+ }
226
+ if (failedResult(result)) {
227
+ unsupported = unsupportedResult(result)
228
+ const error = new Error(result?.timedOut
229
+ ? 'Remote monitor command timed out'
230
+ : result?.stderr || 'Remote monitor command failed')
231
+ error.result = result
232
+ throw error
233
+ }
234
+ results.push(result)
235
+ }
236
+ const sampleTimestamp = Date.now()
237
+ const parsed = parseGroup(name, results, sampleTimestamp, this.networkSample)
238
+ const data = name === 'network' ? parsed?.data : parsed
239
+ if (name === 'network' && parsed?.raw) {
240
+ this.networkSample = parsed.raw
241
+ }
242
+ if (data === null || data === undefined) {
243
+ throw new Error('Remote monitor response could not be parsed')
244
+ }
245
+ runner.failures = 0
246
+ let cpuHistory = this.snapshot.cpuHistory
247
+ if (name === 'cpu' && Number.isFinite(data)) {
248
+ cpuHistory = [
249
+ ...cpuHistory,
250
+ { timestamp: sampleTimestamp, value: data }
251
+ ].slice(-60)
252
+ }
253
+ const levels = { ...this.snapshot.levels }
254
+ if (name === 'cpu') levels.cpu = getUsageLevel(data, levels.cpu)
255
+ if (name === 'memory') levels.memory = getUsageLevel(data.percent, levels.memory)
256
+ if (name === 'disks') {
257
+ levels.disks = Object.fromEntries(data.map(disk => [
258
+ disk.mount, getUsageLevel(disk.percent, levels.disks[disk.mount])
259
+ ]))
260
+ }
261
+ this.snapshot = {
262
+ ...this.snapshot,
263
+ levels,
264
+ cpuHistory,
265
+ groups: {
266
+ ...this.snapshot.groups,
267
+ [name]: {
268
+ status: 'ready',
269
+ data,
270
+ updatedAt: sampleTimestamp,
271
+ error: null
272
+ }
273
+ }
274
+ }
275
+ this.emit()
276
+ } catch (error) {
277
+ if (this.runners.get(name) !== runner) {
278
+ return
279
+ }
280
+ runner.failures += 1
281
+ this.updateGroup(name, {
282
+ status: unsupported
283
+ ? 'unsupported'
284
+ : previous.data !== null && previous.data !== undefined
285
+ ? 'stale'
286
+ : 'error',
287
+ error: error.message || String(error)
288
+ })
289
+ if (unsupported) {
290
+ this.stopGroup(name)
291
+ return
292
+ }
293
+ }
294
+ if (this.runners.get(name) !== runner) {
295
+ return
296
+ }
297
+ const interval = REMOTE_MONITOR_INTERVALS[name]
298
+ if (interval > 0 || runner.failures > 0) {
299
+ const baseInterval = interval || 5000
300
+ const backoff = Math.min(baseInterval * (2 ** runner.failures), 60000)
301
+ runner.timer = setTimeout(() => this.runGroup(name, runner), backoff)
302
+ } else {
303
+ this.runners.delete(name)
304
+ }
305
+ }
306
+
307
+ destroy () {
308
+ clearTimeout(this.cleanupTimer)
309
+ this.cleanupTimer = null
310
+ for (const name of [...this.runners.keys()]) {
311
+ this.stopGroup(name)
312
+ }
313
+ this.subscriptions.clear()
314
+ this.networkSample = null
315
+ }
316
+ }
317
+
318
+ function getSessionMonitor (sessionId) {
319
+ let monitor = monitors.get(sessionId)
320
+ if (!monitor) {
321
+ monitor = new SessionMonitor(sessionId)
322
+ monitors.set(sessionId, monitor)
323
+ }
324
+ return monitor
325
+ }
326
+
327
+ export function subscribeRemoteMonitor (sessionId, groups, listener) {
328
+ return getSessionMonitor(sessionId).subscribe(groups, listener)
329
+ }
330
+
331
+ function documentIsVisible () {
332
+ return typeof document === 'undefined' || document.visibilityState !== 'hidden'
333
+ }
334
+
335
+ export function useRemoteMonitor (sessionId, groups, enabled = true) {
336
+ const [visible, setVisible] = useState(documentIsVisible)
337
+ const [snapshot, setSnapshot] = useState(() => createEmptyMonitorSnapshot(sessionId))
338
+ const groupKey = useMemo(() => [...new Set(groups)].sort().join(','), [groups])
339
+
340
+ useEffect(() => {
341
+ const onVisibilityChange = () => setVisible(documentIsVisible())
342
+ document.addEventListener('visibilitychange', onVisibilityChange)
343
+ return () => document.removeEventListener('visibilitychange', onVisibilityChange)
344
+ }, [])
345
+
346
+ useEffect(() => {
347
+ setSnapshot(createEmptyMonitorSnapshot(sessionId))
348
+ if (!enabled || !visible || !sessionId || !groupKey) {
349
+ return
350
+ }
351
+ return subscribeRemoteMonitor(
352
+ sessionId,
353
+ groupKey.split(',').filter(Boolean),
354
+ setSnapshot
355
+ )
356
+ }, [enabled, groupKey, sessionId, visible])
357
+
358
+ return snapshot.sessionId === sessionId
359
+ ? snapshot
360
+ : createEmptyMonitorSnapshot(sessionId)
361
+ }
@@ -0,0 +1,10 @@
1
+ import { useMemo } from 'react'
2
+ import { MONITOR_ITEM_GROUP } from './monitor-model'
3
+ import { useRemoteMonitor } from './session-monitor'
4
+
5
+ export function useMonitorDetails (sessionId, items, enabled) {
6
+ const itemsKey = items.join(',')
7
+ const groups = useMemo(() => [...new Set(items.map(id => MONITOR_ITEM_GROUP[id]).filter(Boolean))], [itemsKey])
8
+ const snapshot = useRemoteMonitor(sessionId, groups, enabled)
9
+ return { snapshot, levels: snapshot.levels }
10
+ }
@@ -0,0 +1,18 @@
1
+ export function getRemoteMonitorTab (store) {
2
+ return store.tabs.find(tab => tab.id === store.activeTabId) || store.currentTab
3
+ }
4
+
5
+ export function isRemoteMonitorBarVisible (store) {
6
+ const tab = getRemoteMonitorTab(store)
7
+ // Read the pane explicitly so the Manate observer refreshes the global row
8
+ // when an SSH tab switches between terminal and SFTP views.
9
+ const pane = tab?.pane
10
+ const isSsh = tab?.host && (
11
+ tab.type === 'ssh' || tab.type === undefined
12
+ )
13
+ return !!store.config.remoteMonitorBarEnabled &&
14
+ !!isSsh &&
15
+ pane !== undefined &&
16
+ !store.showModal &&
17
+ !(store.rightPanelVisible && store.rightPanelTab === 'info')
18
+ }
@@ -9,7 +9,9 @@ import {
9
9
  ApartmentOutlined,
10
10
  MoreOutlined,
11
11
  ColumnWidthOutlined,
12
- LogoutOutlined
12
+ LogoutOutlined,
13
+ PlusOutlined,
14
+ MinusOutlined
13
15
  } from '@ant-design/icons'
14
16
  import { Tooltip, Popover } from 'antd'
15
17
  import classnames from 'classnames'
@@ -28,6 +30,7 @@ export default function SessionControl (props) {
28
30
  const {
29
31
  tab,
30
32
  isMobile,
33
+ isTouchDevice,
31
34
  isDisabled,
32
35
  isSshDisabled,
33
36
  isNotTerminalType,
@@ -44,7 +47,8 @@ export default function SessionControl (props) {
44
47
  toggleWrap,
45
48
  onFullscreen,
46
49
  onOpenSearch,
47
- onExitGracefully
50
+ onExitGracefully,
51
+ onZoomFontSize
48
52
  } = props
49
53
 
50
54
  if (isNotTerminalType) {
@@ -253,6 +257,28 @@ export default function SessionControl (props) {
253
257
  )
254
258
  }
255
259
 
260
+ function renderFontSizeIcons () {
261
+ // touch screens have no ctrl +/- shortcut to resize the terminal font,
262
+ // so the icons only show there — driven by store.isTouchDevice, which
263
+ // follows the input the user is actually using (see main.jsx).
264
+ if (!isTouchDevice) {
265
+ return null
266
+ }
267
+ const cls = 'mg1r icon-info iblock pointer spliter font-size-control-icon'
268
+ return (
269
+ <>
270
+ <MinusOutlined
271
+ className={cls}
272
+ onClick={() => onZoomFontSize(-1)}
273
+ />
274
+ <PlusOutlined
275
+ className={cls}
276
+ onClick={() => onZoomFontSize(1)}
277
+ />
278
+ </>
279
+ )
280
+ }
281
+
256
282
  function renderTermControls () {
257
283
  const { pane } = tab
258
284
  if (pane !== paneMap.terminal) {
@@ -260,6 +286,7 @@ export default function SessionControl (props) {
260
286
  }
261
287
  return (
262
288
  <div className='fright term-controls'>
289
+ {renderFontSizeIcons()}
263
290
  {renderFullscreenIcon()}
264
291
  {renderSearchIcon()}
265
292
  </div>
@@ -1,14 +1,10 @@
1
1
  /**
2
2
  * terminal/sftp wrapper
3
3
  */
4
- import { createRef } from 'react'
4
+ import { createRef, lazy, Suspense } from 'react'
5
5
  import { Component } from 'manate/react/class-components'
6
6
  import Term from '../terminal/terminal.jsx'
7
7
  import Sftp from '../sftp/sftp-entry'
8
- import RdpSession from '../rdp/rdp-session'
9
- import VncSession from '../vnc/vnc-session'
10
- import WebSession from '../web/web-session.jsx'
11
- import SpiceSession from '../spice/spice-session'
12
8
  import {
13
9
  FullscreenOutlined
14
10
  } from '@ant-design/icons'
@@ -31,8 +27,13 @@ import {
31
27
  import { refs } from '../common/ref'
32
28
  import sanitizeFilename from '../../common/sanitize-filename.js'
33
29
  import SessionControl from './session-control'
30
+ import LazyBoundary from '../common/lazy-boundary'
34
31
  import './session.styl'
35
32
 
33
+ const RdpSession = lazy(() => import('../rdp/rdp-session'))
34
+ const VncSession = lazy(() => import('../vnc/vnc-session'))
35
+ const WebSession = lazy(() => import('../web/web-session.jsx'))
36
+ const SpiceSession = lazy(() => import('../spice/spice-session'))
36
37
  const e = window.translate
37
38
  const SplitterPane = Splitter.Panel
38
39
 
@@ -239,9 +240,11 @@ export default class SessionWrapper extends Component {
239
240
  reloadTab: this.props.reloadTab
240
241
  }
241
242
  return (
242
- <WebSession
243
- {...webProps}
244
- />
243
+ <LazyBoundary>
244
+ <WebSession
245
+ {...webProps}
246
+ />
247
+ </LazyBoundary>
245
248
  )
246
249
  }
247
250
  if (type === terminalRdpType || type === terminalVncType || type === terminalSpiceType) {
@@ -269,23 +272,29 @@ export default class SessionWrapper extends Component {
269
272
  }
270
273
  if (type === terminalVncType) {
271
274
  return (
272
- <VncSession
273
- {...rdpProps}
274
- />
275
+ <LazyBoundary>
276
+ <VncSession
277
+ {...rdpProps}
278
+ />
279
+ </LazyBoundary>
275
280
  )
276
281
  }
277
282
  if (type === terminalSpiceType) {
278
283
  return (
279
- <SpiceSession
280
- {...rdpProps}
281
- />
284
+ <LazyBoundary>
285
+ <SpiceSession
286
+ {...rdpProps}
287
+ />
288
+ </LazyBoundary>
282
289
  )
283
290
  }
284
291
 
285
292
  return (
286
- <RdpSession
287
- {...rdpProps}
288
- />
293
+ <LazyBoundary>
294
+ <RdpSession
295
+ {...rdpProps}
296
+ />
297
+ </LazyBoundary>
289
298
  )
290
299
  }
291
300
 
@@ -471,6 +480,15 @@ export default class SessionWrapper extends Component {
471
480
  refs.get('term-' + this.props.tab.id)?.toggleSearch()
472
481
  }
473
482
 
483
+ handleZoomFontSize = (v) => {
484
+ const term = refs.get('term-' + this.props.tab.id)
485
+ if (!term?.term) {
486
+ return
487
+ }
488
+ term.zoom(v)
489
+ term.term.focus()
490
+ }
491
+
474
492
  toggleWrap = () => {
475
493
  const termRef = refs.get('term-' + this.props.tab.id)
476
494
  if (!termRef?.term) {
@@ -598,6 +616,7 @@ export default class SessionWrapper extends Component {
598
616
  <SessionControl
599
617
  tab={this.props.tab}
600
618
  isMobile={window.store.isMobile}
619
+ isTouchDevice={window.store.isTouchDevice}
601
620
  isDisabled={this.isDisabled()}
602
621
  isSshDisabled={this.isSshDisabled()}
603
622
  isNotTerminalType={this.isNotTerminalType()}
@@ -613,10 +632,13 @@ export default class SessionWrapper extends Component {
613
632
  toggleBroadcastInput={this.toggleBroadcastInput}
614
633
  toggleWrap={this.toggleWrap}
615
634
  onFullscreen={this.handleFullscreen}
635
+ onZoomFontSize={this.handleZoomFontSize}
616
636
  onOpenSearch={this.handleOpenSearch}
617
637
  onExitGracefully={this.handleExitGracefully}
618
638
  />
619
- {this.renderViews()}
639
+ <Suspense fallback={null}>
640
+ {this.renderViews()}
641
+ </Suspense>
620
642
  </div>
621
643
  )
622
644
  }
@@ -35,7 +35,7 @@ export default class Sessions extends Component {
35
35
  }
36
36
 
37
37
  computeSessionStyle = (batch) => {
38
- const style = this.props.styles[batch]
38
+ const style = this.props.styles[batch] || this.props.styles[0] || {}
39
39
  return pixed(style)
40
40
  }
41
41
 
@@ -46,9 +46,12 @@ export default class Sessions extends Component {
46
46
  activeTabId,
47
47
  sizes
48
48
  } = this.props
49
+ const fallbackSize = sizes[0] || { height: 0, width: 0 }
49
50
  return tabs.map((tab) => {
50
51
  const { id, batch } = tab
51
- const { height, width } = sizes[batch]
52
+ // MCP/AI-created tabs may carry an out-of-range or missing batch.
53
+ // Never crash the whole UI on bad data — fall back to batch 0 size.
54
+ const { height, width } = sizes[batch] || fallbackSize
52
55
  const currentBatchTabId = this.props['activeTabId' + batch]
53
56
  const cls = classNames(
54
57
  `session-wrap session-${id}`,