@electerm/electerm-react 1.70.6 → 1.72.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 (62) hide show
  1. package/client/common/cache.js +56 -0
  2. package/client/common/constants.js +2 -0
  3. package/client/common/download.jsx +5 -7
  4. package/client/common/setting-list.js +27 -0
  5. package/client/components/ai/ai-cache.jsx +36 -0
  6. package/client/components/ai/ai-chat-history-item.jsx +1 -1
  7. package/client/components/ai/ai-chat.jsx +5 -40
  8. package/client/components/ai/ai-config-props.js +7 -0
  9. package/client/components/ai/ai-config.jsx +5 -14
  10. package/client/components/ai/ai.styl +0 -4
  11. package/client/components/ai/providers.js +2 -2
  12. package/client/components/batch-op/batch-op-entry.jsx +1 -1
  13. package/client/components/batch-op/batch-op.jsx +2 -2
  14. package/client/components/bookmark-form/form-ssh-common.jsx +2 -3
  15. package/client/components/bookmark-form/form-tabs.jsx +2 -2
  16. package/client/components/bookmark-form/render-connection-hopping.jsx +2 -2
  17. package/client/components/bookmark-form/render-delayed-scripts.jsx +4 -4
  18. package/client/components/bookmark-form/render-ssh-tunnel.jsx +2 -2
  19. package/client/components/bookmark-form/use-quick-commands.jsx +2 -2
  20. package/client/components/common/input-auto-focus.jsx +3 -11
  21. package/client/components/main/main.jsx +5 -0
  22. package/client/components/profile/profile-form-elem.jsx +1 -3
  23. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -3
  24. package/client/components/quick-commands/quick-commands-list-form.jsx +2 -2
  25. package/client/components/session/session.jsx +49 -14
  26. package/client/components/session/session.styl +10 -3
  27. package/client/components/session/sessions.jsx +1 -1
  28. package/client/components/setting-panel/keywords-form.jsx +36 -38
  29. package/client/components/setting-panel/setting-modal.jsx +2 -2
  30. package/client/components/setting-panel/tab-settings.jsx +26 -0
  31. package/client/components/sftp/address-bar.jsx +9 -2
  32. package/client/components/sftp/address-bookmark.jsx +4 -6
  33. package/client/components/sftp/keyword-filter.jsx +63 -0
  34. package/client/components/sftp/list-table-ui.jsx +7 -9
  35. package/client/components/sftp/sftp-entry.jsx +45 -8
  36. package/client/components/sftp/sftp.styl +6 -1
  37. package/client/components/shortcuts/shortcut-control.jsx +20 -0
  38. package/client/components/shortcuts/shortcut-editor.jsx +2 -2
  39. package/client/components/shortcuts/shortcuts.jsx +2 -2
  40. package/client/components/sidebar/info-modal.jsx +2 -2
  41. package/client/components/sidebar/transfer-list-control.jsx +18 -20
  42. package/client/components/ssh-config/ssh-config-item.jsx +2 -4
  43. package/client/components/ssh-config/ssh-config-load-notify.jsx +2 -2
  44. package/client/components/sys-menu/zoom.jsx +2 -2
  45. package/client/components/tabs/index.jsx +1 -1
  46. package/client/components/tabs/tab.jsx +3 -3
  47. package/client/components/terminal/cmd-item.jsx +32 -0
  48. package/client/components/terminal/command-tracker-addon.js +3 -1
  49. package/client/components/terminal/term-search.jsx +5 -6
  50. package/client/components/terminal/terminal-command-dropdown.jsx +303 -0
  51. package/client/components/terminal/terminal.jsx +84 -3
  52. package/client/components/terminal/terminal.styl +58 -0
  53. package/client/components/terminal-info/terminal-info.jsx +2 -2
  54. package/client/components/tree-list/tree-list.jsx +1 -1
  55. package/client/components/web/address-bar.jsx +2 -2
  56. package/client/store/common.js +27 -2
  57. package/client/store/init-state.js +3 -3
  58. package/client/store/item.js +2 -1
  59. package/client/store/setting.js +3 -2
  60. package/client/store/store.js +23 -24
  61. package/client/store/watch.js +7 -1
  62. package/package.json +1 -1
@@ -140,7 +140,7 @@ export default class Shortcuts extends PureComponent {
140
140
  rowKey: 'id'
141
141
  }
142
142
  return (
143
- <div>
143
+ <>
144
144
  <Table
145
145
  {...props}
146
146
  />
@@ -151,7 +151,7 @@ export default class Shortcuts extends PureComponent {
151
151
  {e('resetAllToDefault')}
152
152
  </Button>
153
153
  </div>
154
- </div>
154
+ </>
155
155
  )
156
156
  }
157
157
  }
@@ -119,7 +119,7 @@ export default auto(function InfoModal (props) {
119
119
  key: infoTabs.info,
120
120
  label: e('about'),
121
121
  children: (
122
- <div>
122
+ <>
123
123
  <LogoElem />
124
124
  <p className='mg2b'>{e('desc')}</p>
125
125
  <RunningTime />
@@ -187,7 +187,7 @@ export default auto(function InfoModal (props) {
187
187
  </Link>
188
188
  </p>
189
189
  {renderCheckUpdate()}
190
- </div>
190
+ </>
191
191
  )
192
192
  },
193
193
  {
@@ -137,26 +137,24 @@ export default class TransferModalUI extends Component {
137
137
  ...groups
138
138
  ]
139
139
  return (
140
- <div>
141
- <Select
142
- value={this.state.filter}
143
- onChange={this.handleFilter}
144
- popupMatchSelectWidth={false}
145
- >
146
- {
147
- all.map(item => {
148
- return (
149
- <Option
150
- key={item.id}
151
- value={item.id}
152
- >
153
- {item.title}
154
- </Option>
155
- )
156
- })
157
- }
158
- </Select>
159
- </div>
140
+ <Select
141
+ value={this.state.filter}
142
+ onChange={this.handleFilter}
143
+ popupMatchSelectWidth={false}
144
+ >
145
+ {
146
+ all.map(item => {
147
+ return (
148
+ <Option
149
+ key={item.id}
150
+ value={item.id}
151
+ >
152
+ {item.title}
153
+ </Option>
154
+ )
155
+ })
156
+ }
157
+ </Select>
160
158
  )
161
159
  }
162
160
 
@@ -16,10 +16,8 @@ export default function SshConfigItem (props) {
16
16
 
17
17
  return (
18
18
  <Tooltip title={generateTooltipContent(item)}>
19
- <div>
20
- <div className='elli pd1y pd2x'>
21
- ssh {item.title}
22
- </div>
19
+ <div className='elli pd1y pd2x'>
20
+ ssh {item.title}
23
21
  </div>
24
22
  </Tooltip>
25
23
  )
@@ -25,7 +25,7 @@ function showNotification () {
25
25
  placement: 'bottom',
26
26
  key: 'sshConfigNotify',
27
27
  description: (
28
- <div>
28
+ <>
29
29
  <p>{e('sshConfigNotice')}</p>
30
30
  <Button type='primary' onClick={handleLoad} className='mg1r mg1b'>
31
31
  {e('import')}
@@ -33,7 +33,7 @@ function showNotification () {
33
33
  <Button onClick={handleIgnore} className='mg1r mg1b'>
34
34
  {e('ignore')}
35
35
  </Button>
36
- </div>
36
+ </>
37
37
  )
38
38
  })
39
39
  }
@@ -20,7 +20,7 @@ export default function ZoomMenu (props) {
20
20
  max={500}
21
21
  addonAfter='%'
22
22
  addonBefore={
23
- <div>
23
+ <>
24
24
  <PlusCircleOutlined
25
25
  onClick={() => store.zoom(0.25, true)}
26
26
  className='mg1r pointer font16'
@@ -29,7 +29,7 @@ export default function ZoomMenu (props) {
29
29
  onClick={() => store.zoom(-0.25, true)}
30
30
  className='pointer font16'
31
31
  />
32
- </div>
32
+ </>
33
33
  }
34
34
  />
35
35
  )
@@ -2,7 +2,7 @@
2
2
  * session tabs component
3
3
  */
4
4
 
5
- import { Component } from '../common/component'
5
+ import { Component } from 'manate/react/class-components'
6
6
  import React from 'react'
7
7
  import runIdle from '../../common/run-idle'
8
8
  import { throttle } from 'lodash-es'
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { createRef } from 'react'
6
- import { Component } from '../common/component'
6
+ import { Component } from 'manate/react/class-components'
7
7
  import { refs } from '../common/ref'
8
8
  import {
9
9
  CloseOutlined,
@@ -373,10 +373,10 @@ class Tab extends Component {
373
373
  return <div key={tunnel}>{tunnel}</div>
374
374
  })
375
375
  return (
376
- <div>
376
+ <>
377
377
  <div>{title}</div>
378
378
  {list}
379
- </div>
379
+ </>
380
380
  )
381
381
  }
382
382
 
@@ -0,0 +1,32 @@
1
+ import React from 'react'
2
+ import { CloseCircleOutlined } from '@ant-design/icons'
3
+
4
+ const SuggestionItem = ({ item, onSelect, onDelete }) => {
5
+ const handleClick = () => {
6
+ onSelect(item)
7
+ }
8
+
9
+ const handleDelete = (e) => {
10
+ e.stopPropagation()
11
+ onDelete(item)
12
+ }
13
+
14
+ return (
15
+ <div className='suggestion-item'>
16
+ <span className='suggestion-command' onClick={handleClick}>
17
+ {item.command}
18
+ </span>
19
+ <span className='suggestion-type'>
20
+ {item.type}
21
+ </span>
22
+ {item.type === 'H' && (
23
+ <CloseCircleOutlined
24
+ className='suggestion-delete'
25
+ onClick={handleDelete}
26
+ />
27
+ )}
28
+ </div>
29
+ )
30
+ }
31
+
32
+ export default SuggestionItem
@@ -45,7 +45,9 @@ export class CommandTrackerAddon {
45
45
  // This is now our internal handler
46
46
  _handleKey = (e) => {
47
47
  const { key } = e
48
- if (key === 'Enter') {
48
+ if (e.ctrlKey && key.toLowerCase() === 'c') {
49
+ this.clearCommand()
50
+ } else if (key === 'Enter') {
49
51
  // Command executed, reset
50
52
  this.currentCommand = this.activeCommand
51
53
  this.activeCommand = ''
@@ -127,14 +127,14 @@ export default class TermSearch extends PureComponent {
127
127
 
128
128
  renderAfter = () => {
129
129
  return (
130
- <div>
130
+ <>
131
131
  {
132
132
  this.renderMatchData()
133
133
  }
134
134
  {
135
135
  this.searchActions.map(this.renderSearchAction)
136
136
  }
137
- </div>
137
+ </>
138
138
  )
139
139
  }
140
140
 
@@ -174,11 +174,11 @@ export default class TermSearch extends PureComponent {
174
174
 
175
175
  renderSuffix = () => {
176
176
  return (
177
- <div>
177
+ <>
178
178
  {
179
179
  this.searchControls.map(this.renderSearchControl)
180
180
  }
181
- </div>
181
+ </>
182
182
  )
183
183
  }
184
184
 
@@ -191,8 +191,7 @@ export default class TermSearch extends PureComponent {
191
191
  if (
192
192
  !termSearchOpen ||
193
193
  !currentTab ||
194
- currentTab.pane === paneMap.fileManager ||
195
- currentTab.pane === paneMap.sftp
194
+ currentTab.pane === paneMap.fileManager
196
195
  ) {
197
196
  return null
198
197
  }
@@ -0,0 +1,303 @@
1
+ import { Component } from 'manate/react/class-components'
2
+ import { refsStatic, refs } from '../common/ref'
3
+ import SuggestionItem from './cmd-item'
4
+ import { aiSuggestionsCache } from '../../common/cache'
5
+ import uid from '../../common/uid'
6
+ import classnames from 'classnames'
7
+ import {
8
+ LoadingOutlined
9
+ } from '@ant-design/icons'
10
+
11
+ export default class TerminalCmdSuggestions extends Component {
12
+ state = {
13
+ cursorPosition: {},
14
+ showSuggestions: false,
15
+ loadingAiSuggestions: false,
16
+ aiSuggestions: [],
17
+ cmdIsDescription: false,
18
+ reverse: false,
19
+ cmd: ''
20
+ }
21
+
22
+ componentDidMount () {
23
+ refsStatic.add('terminal-suggestions', this)
24
+ }
25
+
26
+ componentWillUnmount () {
27
+ refsStatic.remove('terminal-suggestions')
28
+ }
29
+
30
+ parseAiSuggestions = (aiResponse) => {
31
+ try {
32
+ return JSON.parse(aiResponse.response).map(d => {
33
+ return {
34
+ command: d,
35
+ type: 'AI'
36
+ }
37
+ })
38
+ } catch (e) {
39
+ console.log('parseAiSuggestions error:', e)
40
+ return []
41
+ }
42
+ }
43
+
44
+ getAiSuggestions = async (event) => {
45
+ event.stopPropagation()
46
+ const { cmd } = this.state
47
+ if (window.store.aiConfigMissing()) {
48
+ window.store.toggleAIConfig()
49
+ }
50
+ this.setState({
51
+ loadingAiSuggestions: true
52
+ })
53
+ const {
54
+ config
55
+ } = window.store
56
+ const prompt = `give me max 5 command suggestions for user input: "${cmd}", return pure json format result only, no extra words, no markdown format, follow this format: ["command1","command2"...]`
57
+ const cached = aiSuggestionsCache.get(cmd)
58
+ if (cached) {
59
+ this.setState({
60
+ loadingAiSuggestions: false,
61
+ aiSuggestions: cached
62
+ })
63
+ return
64
+ }
65
+
66
+ const aiResponse = aiSuggestionsCache.get(prompt) || await window.pre.runGlobalAsync(
67
+ 'AIchat',
68
+ prompt,
69
+ config.modelAI,
70
+ config.roleAI,
71
+ config.baseURLAI,
72
+ config.apiPathAI,
73
+ config.apiKeyAI
74
+ ).catch(
75
+ window.store.onError
76
+ )
77
+ if (cmd !== this.state.cmd) {
78
+ this.setState({
79
+ loadingAiSuggestions: false
80
+ })
81
+ return
82
+ }
83
+ if (aiResponse && aiResponse.error) {
84
+ this.setState({
85
+ loadingAiSuggestions: false
86
+ })
87
+ return window.store.onError(
88
+ new Error(aiResponse.error)
89
+ )
90
+ }
91
+ this.setState({
92
+ loadingAiSuggestions: false,
93
+ aiSuggestions: this.parseAiSuggestions(aiResponse, cmd)
94
+ })
95
+ }
96
+
97
+ openSuggestions = (cursorPosition, cmd) => {
98
+ if (!this.state.showSuggestions) {
99
+ document.addEventListener('click', this.handleClickOutside)
100
+ document.addEventListener('keydown', this.handleKeyDown)
101
+ }
102
+
103
+ const {
104
+ left,
105
+ top,
106
+ cellHeight
107
+ } = cursorPosition
108
+ const w = window.innerWidth
109
+ const h = window.innerHeight
110
+
111
+ const position = {}
112
+ const reverse = top > h / 2
113
+
114
+ // Use right position if close to right edge
115
+ if (left > w / 2) {
116
+ position.right = w - left
117
+ } else {
118
+ position.left = left
119
+ }
120
+
121
+ // Use bottom position if close to bottom edge
122
+ if (reverse) {
123
+ position.bottom = h - top + cellHeight
124
+ } else {
125
+ position.top = top
126
+ }
127
+ this.setState({
128
+ showSuggestions: true,
129
+ cursorPosition: position,
130
+ cmd,
131
+ reverse
132
+ })
133
+ }
134
+
135
+ closeSuggestions = () => {
136
+ document.removeEventListener('click', this.handleClickOutside)
137
+ document.removeEventListener('keydown', this.handleKeyDown)
138
+ const {
139
+ aiSuggestions
140
+ } = this.state
141
+ if (aiSuggestions.length) {
142
+ aiSuggestionsCache.set(this.state.cmd, aiSuggestions)
143
+ aiSuggestions.forEach(item => {
144
+ window.store.addCmdHistory(item.command, 'aiCmdHistory')
145
+ })
146
+ }
147
+ this.setState({
148
+ showSuggestions: false,
149
+ aiSuggestions: []
150
+ })
151
+ }
152
+
153
+ handleClickOutside = (event) => {
154
+ const suggestionElement = document.querySelector('.terminal-suggestions-wrap')
155
+ if (suggestionElement && !suggestionElement.contains(event.target)) {
156
+ this.closeSuggestions()
157
+ }
158
+ }
159
+
160
+ handleKeyDown = (event) => {
161
+ if (event.key === 'Escape') {
162
+ this.closeSuggestions()
163
+ }
164
+ }
165
+
166
+ handleDelete = (item) => {
167
+ window.store.terminalCommandHistory.delete(item.command)
168
+ }
169
+
170
+ handleSelect = (item) => {
171
+ const { activeTabId } = window.store
172
+ const terminal = refs.get('term-' + activeTabId)
173
+ if (!terminal) {
174
+ return
175
+ }
176
+
177
+ // const titleElement = domEvent.target.closest('.ant-menu-title-content')
178
+ // const command = titleElement?.firstChild?.textContent
179
+ const { command } = item
180
+ const { cmd } = this.state
181
+ let txt = ''
182
+ if (cmd && command.startsWith(cmd)) {
183
+ txt = command.slice(cmd.length)
184
+ } else {
185
+ const pre = '\b'.repeat(cmd.length)
186
+ txt = pre + command
187
+ }
188
+ terminal.attachAddon._sendData(txt)
189
+ terminal.term.focus()
190
+ this.closeSuggestions()
191
+ }
192
+
193
+ processCommands = (commands, type, uniqueCommands, res) => {
194
+ const { cmd } = this.state
195
+ commands
196
+ .filter(command => command.startsWith(cmd))
197
+ .forEach(command => {
198
+ if (!uniqueCommands.has(command)) {
199
+ uniqueCommands.add(command)
200
+ res.push({
201
+ id: uid(),
202
+ command,
203
+ type
204
+ })
205
+ }
206
+ })
207
+ }
208
+
209
+ getSuggestions = () => {
210
+ const uniqueCommands = new Set()
211
+ const {
212
+ history = [],
213
+ batch = [],
214
+ quick = []
215
+ } = this.props.suggestions
216
+ const res = []
217
+ this.state.aiSuggestions
218
+ .forEach(item => {
219
+ if (!uniqueCommands.has(item.command)) {
220
+ uniqueCommands.add(item.command)
221
+ }
222
+ res.push({
223
+ id: uid(),
224
+ ...item
225
+ })
226
+ })
227
+ this.processCommands(history, 'H', uniqueCommands, res)
228
+ this.processCommands(batch, 'B', uniqueCommands, res)
229
+ this.processCommands(quick, 'Q', uniqueCommands, res)
230
+ return this.state.reverse ? res.reverse() : res
231
+ }
232
+
233
+ renderAIIcon () {
234
+ const e = window.translate
235
+ const {
236
+ loadingAiSuggestions
237
+ } = this.state
238
+ if (loadingAiSuggestions) {
239
+ return (
240
+ <>
241
+ <LoadingOutlined /> {e('getAiSuggestions')}
242
+ </>
243
+ )
244
+ }
245
+ const aiProps = {
246
+ onClick: this.getAiSuggestions,
247
+ className: 'pointer'
248
+ }
249
+ return (
250
+ <div {...aiProps}>
251
+ {e('getAiSuggestions')}
252
+ </div>
253
+ )
254
+ }
255
+
256
+ renderSticky (pos) {
257
+ const {
258
+ reverse
259
+ } = this.state
260
+ if (
261
+ (pos === 'top' && !reverse) ||
262
+ (pos === 'bottom' && reverse)
263
+ ) {
264
+ return null
265
+ }
266
+ return (
267
+ <div className='terminal-suggestions-sticky'>
268
+ {this.renderAIIcon()}
269
+ </div>
270
+ )
271
+ }
272
+
273
+ render () {
274
+ const { showSuggestions, cursorPosition, reverse } = this.state
275
+ if (!showSuggestions) {
276
+ return null
277
+ }
278
+ const suggestions = this.getSuggestions()
279
+ const cls = classnames('terminal-suggestions-wrap', {
280
+ reverse
281
+ })
282
+ return (
283
+ <div className={cls} style={cursorPosition}>
284
+ {this.renderSticky('top')}
285
+ <div className='terminal-suggestions-list'>
286
+ {
287
+ suggestions.map(item => {
288
+ return (
289
+ <SuggestionItem
290
+ key={item.id}
291
+ item={item}
292
+ onSelect={this.handleSelect}
293
+ onDelete={this.handleDelete}
294
+ />
295
+ )
296
+ })
297
+ }
298
+ </div>
299
+ {this.renderSticky('bottom')}
300
+ </div>
301
+ )
302
+ }
303
+ }
@@ -50,7 +50,7 @@ import AIIcon from '../icons/ai-icon.jsx'
50
50
  import { formatBytes } from '../../common/byte-format.js'
51
51
  import * as fs from './fs.js'
52
52
  import iconsMap from '../sys-menu/icons-map.jsx'
53
- import { refs } from '../common/ref.js'
53
+ import { refs, refsStatic } from '../common/ref.js'
54
54
  import createDefaultLogPath from '../../common/default-log-path.js'
55
55
 
56
56
  const e = window.translate
@@ -255,7 +255,7 @@ clear\r`
255
255
 
256
256
  isActiveTerminal = () => {
257
257
  return this.props.tab.id === this.props.activeTabId &&
258
- this.props.pane === paneMap.terminal
258
+ this.props.tab.pane === paneMap.terminal
259
259
  }
260
260
 
261
261
  clearShortcut = (e) => {
@@ -822,14 +822,56 @@ clear\r`
822
822
  this.props.setCwd(cwd, this.state.id)
823
823
  }
824
824
 
825
+ getCursorPosition = () => {
826
+ if (!this.term) return null
827
+
828
+ // Get the active buffer and cursor position
829
+ const buffer = this.term.buffer.active
830
+ const cursorRow = buffer.cursorY
831
+ const cursorCol = buffer.cursorX
832
+
833
+ // Get dimensions from term element
834
+ const termElement = this.term.element
835
+ if (!termElement) return null
836
+
837
+ // Get the exact position of the terminal element
838
+ const termRect = termElement.getBoundingClientRect()
839
+
840
+ // Calculate cell dimensions
841
+ const cellWidth = termRect.width / this.term.cols
842
+ const cellHeight = termRect.height / this.term.rows
843
+
844
+ // Calculate absolute position relative to terminal element
845
+ const left = Math.floor(termRect.left + (cursorCol * cellWidth))
846
+ const top = Math.floor(termRect.top + ((cursorRow + 1) * cellHeight))
847
+
848
+ return {
849
+ cellWidth,
850
+ cellHeight,
851
+ left,
852
+ top
853
+ }
854
+ }
855
+
856
+ closeSuggestions = () => {
857
+ refsStatic
858
+ .get('terminal-suggestions')
859
+ ?.closeSuggestions()
860
+ }
861
+
825
862
  onData = (d) => {
826
863
  if (this.cmdAddon) {
827
864
  this.cmdAddon.handleData(d)
828
865
  }
866
+ const data = this.getCmd().trim()
829
867
  if (!d.includes('\r')) {
830
868
  delete this.userTypeExit
869
+ const cursorPos = this.getCursorPosition()
870
+ refsStatic
871
+ .get('terminal-suggestions')
872
+ ?.openSuggestions(cursorPos, data)
831
873
  } else {
832
- const data = this.getCmd().trim()
874
+ this.closeSuggestions()
833
875
  if (this.term.buffer.active.type !== 'alternate') {
834
876
  this.timers.getCwd = setTimeout(this.getCwd, 200)
835
877
  }
@@ -837,6 +879,7 @@ clear\r`
837
879
  'exit',
838
880
  'logout'
839
881
  ]
882
+ window.store.addCmdHistory(data)
840
883
  if (exitCmds.includes(data)) {
841
884
  this.userTypeExit = true
842
885
  this.timers.userTypeExit = setTimeout(() => {
@@ -1083,6 +1126,7 @@ clear\r`
1083
1126
  socket.onclose = this.oncloseSocket
1084
1127
  socket.onerror = this.onerrorSocket
1085
1128
  this.socket = socket
1129
+ this.initSocketEvents()
1086
1130
  this.term = term
1087
1131
  socket.onopen = () => {
1088
1132
  this.initAttachAddon()
@@ -1106,6 +1150,43 @@ clear\r`
1106
1150
  )
1107
1151
  }
1108
1152
 
1153
+ initSocketEvents = () => {
1154
+ const originalSend = this.socket.send
1155
+ this.socket.send = (data) => {
1156
+ // Call original send first
1157
+ originalSend.call(this.socket, data)
1158
+
1159
+ // Broadcast to other terminals
1160
+ this.broadcastSocketData(data)
1161
+ }
1162
+ }
1163
+
1164
+ canReceiveBroadcast = (termRef) => {
1165
+ const tabId = termRef.props?.tab?.id
1166
+ const isActiveInBatch = termRef.props.currentBatchTabId === tabId
1167
+ return (
1168
+ isActiveInBatch &&
1169
+ termRef.socket &&
1170
+ termRef.props?.tab.pane === paneMap.terminal
1171
+ )
1172
+ }
1173
+
1174
+ broadcastSocketData = (data) => {
1175
+ if (!this.isActiveTerminal() || !this.props.broadcastInput) {
1176
+ return
1177
+ }
1178
+
1179
+ window.refs.forEach((termRef, refId) => {
1180
+ if (
1181
+ refId !== this.id &&
1182
+ refId.startsWith('term-') &&
1183
+ this.canReceiveBroadcast(termRef)
1184
+ ) {
1185
+ termRef.socket.send(data)
1186
+ }
1187
+ })
1188
+ }
1189
+
1109
1190
  onResize = throttle(() => {
1110
1191
  const cid = this.props.currentBatchTabId
1111
1192
  const tid = this.props.tab?.id