@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
@@ -0,0 +1,96 @@
1
+ import { pick } from 'lodash-es'
2
+ import { notification } from '../../common/notification'
3
+ import ShowItem from '../../common/show-item.jsx'
4
+ import { refs } from '../../common/ref.js'
5
+ import { startTerminalLogFile, toggleTerminalLog } from '../terminal-apis.js'
6
+ import { getFolderFromFilePath } from '../../sftp/file-read.js'
7
+
8
+ const e = window.translate
9
+
10
+ /**
11
+ * Session logging / recording: dumping the buffer to disk and keeping the
12
+ * timestamped log file running.
13
+ */
14
+ export const logMixin = {
15
+ syncTermInfo (stateUpdate) {
16
+ this.setState(stateUpdate)
17
+ const infoUpdate = pick(stateUpdate, ['saveTerminalLogToFile', 'addTimeStampToTermLog', 'logPath', 'logFileName'])
18
+ if (Object.keys(infoUpdate).length) {
19
+ refs.get('term-info-' + this.props.tab.id)?.setState(infoUpdate)
20
+ }
21
+ },
22
+
23
+ async openLogSaveDialog (titleKey) {
24
+ const { logName } = this.props
25
+ const result = await window.api.saveDialog({
26
+ title: e(titleKey),
27
+ defaultPath: logName + '.log',
28
+ filters: [
29
+ { name: 'Log files', extensions: ['log'] }
30
+ ],
31
+ properties: ['createDirectory', 'showOverwriteConfirmation']
32
+ })
33
+ if (result.canceled || !result.filePath) {
34
+ return null
35
+ }
36
+ return result.filePath
37
+ },
38
+
39
+ async onSaveTerminalLog () {
40
+ const filePath = await this.openLogSaveDialog('saveTerminalLogToFile')
41
+ if (!filePath) {
42
+ return
43
+ }
44
+ const content = this.getTerminalBufferText()
45
+ await window.fs.writeFile(filePath, content).catch(window.store.onError)
46
+ const { addTimeStampToTermLog } = this.state
47
+ startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
48
+ const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
49
+ this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
50
+ notification.success({
51
+ message: e('saveTerminalLogToFile'),
52
+ description: <ShowItem to={filePath}>{filePath}</ShowItem>,
53
+ duration: 5
54
+ })
55
+ },
56
+
57
+ async onRecord () {
58
+ const filePath = await this.openLogSaveDialog('record')
59
+ if (!filePath) {
60
+ return
61
+ }
62
+ const { addTimeStampToTermLog } = this.state
63
+ startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
64
+ const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
65
+ this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
66
+ this.setState({ recording: true, recordingFilePath: filePath })
67
+ notification.success({
68
+ message: e('record'),
69
+ description: <ShowItem to={filePath}>{filePath}</ShowItem>,
70
+ duration: 5
71
+ })
72
+ },
73
+
74
+ onStopRecord () {
75
+ const { recordingFilePath } = this.state
76
+ toggleTerminalLog(this.pid).catch(window.store.onError)
77
+ this.syncTermInfo({ saveTerminalLogToFile: false })
78
+ this.setState({ recording: false, recordingFilePath: '' })
79
+ notification.success({
80
+ message: e('stopRecord'),
81
+ description: <ShowItem to={recordingFilePath}>{recordingFilePath}</ShowItem>
82
+ })
83
+ },
84
+
85
+ handleShowInfo () {
86
+ const { logName, tab } = this.props
87
+ const infoProps = {
88
+ logName,
89
+ id: tab.id,
90
+ pid: tab.id,
91
+ isRemote: this.isRemote(),
92
+ isActive: this.isActiveTerminal()
93
+ }
94
+ Object.assign(window.store.terminalInfoProps, infoProps)
95
+ }
96
+ }
@@ -0,0 +1,87 @@
1
+ import { throttle } from 'lodash-es'
2
+ import { resizeTerm } from '../terminal-apis.js'
3
+ import { minTerminalFontSize } from '../../../common/constants.js'
4
+
5
+ /**
6
+ * Sizing: fitting the terminal into its (possibly hidden) container, font
7
+ * zoom, and keeping the remote pty in sync with the on-screen size.
8
+ */
9
+ export const resizeMixin = {
10
+ // 判断终端挂载容器当前是否真实可见(非 display:none)。
11
+ // 隐藏标签的 clientWidth/clientHeight 为 0,此时不应 fit,否则会算出 0 列。
12
+ isElementVisible () {
13
+ const el = this.domRef.current
14
+ if (!el) {
15
+ return false
16
+ }
17
+ return el.clientWidth > 0 && el.clientHeight > 0
18
+ },
19
+
20
+ // 重新适配终端尺寸并强制重绘可见区域。
21
+ // 仅当容器真正可见时才 fit:隐藏标签(display:none)下 clientWidth 为 0,
22
+ // 若在此时 fit 会算出 0/极小列数,使 shell 提示符被逐字符错误换行,
23
+ // 出现快速连续打开多个连接时的虚假/重复提示符。因此隐藏态跳过 fit,
24
+ // 保留 xterm 默认 80x24,待标签激活可见后再 fit 重绘,从根上避免换行损坏。
25
+ fitAndRefresh () {
26
+ if (!this.term || !this.fitAddon || this.onClose) {
27
+ return
28
+ }
29
+ if (!this.isElementVisible()) {
30
+ return
31
+ }
32
+ try {
33
+ this.fitAddon.fit()
34
+ this.term.refresh(0, this.term.rows - 1)
35
+ } catch (e) {
36
+ console.info('resize failed', e)
37
+ }
38
+ },
39
+
40
+ // One throttled runner per terminal: a module level one would let tabs
41
+ // throttle each other.
42
+ onResize () {
43
+ if (!this._resizeThrottled) {
44
+ this._resizeThrottled = throttle(() => this.fitAndRefresh(), 200)
45
+ }
46
+ this._resizeThrottled()
47
+ },
48
+
49
+ onResizeTerminal (size) {
50
+ const { cols, rows } = size
51
+ resizeTerm(this.pid, cols, rows)
52
+ },
53
+
54
+ zoom (v) {
55
+ const { term } = this
56
+ if (!term) {
57
+ return
58
+ }
59
+ const next = Math.max(
60
+ minTerminalFontSize,
61
+ term.options.fontSize + v
62
+ )
63
+ const changed = next - term.options.fontSize
64
+ if (changed === 0) {
65
+ return
66
+ }
67
+ term.options.fontSize = next
68
+ window.store.triggerResize()
69
+ if (this.originalFontSize == null) {
70
+ this.originalFontSize = next - changed
71
+ }
72
+ this.setState({
73
+ fontSizeChanged: next !== this.originalFontSize
74
+ })
75
+ },
76
+
77
+ handleResetFontSize () {
78
+ const { term } = this
79
+ if (!term || this.originalFontSize == null) {
80
+ return
81
+ }
82
+ term.options.fontSize = this.originalFontSize
83
+ window.store.triggerResize()
84
+ this.setState({ fontSizeChanged: false })
85
+ term.focus()
86
+ }
87
+ }
@@ -0,0 +1,433 @@
1
+ import { pick } from 'lodash-es'
2
+ import clone from '../../../common/to-simple-obj.js'
3
+ import deepCopy from 'json-deep-copy'
4
+ import {
5
+ statusMap,
6
+ typeMap,
7
+ isWin,
8
+ isMac,
9
+ terminalSerialType,
10
+ paneMap
11
+ } from '../../../common/constants.js'
12
+ import getProxy from '../../../common/get-proxy.js'
13
+ import { refs } from '../../common/ref.js'
14
+ import { createTerm, resizeTerm } from '../terminal-apis.js'
15
+ import { ZmodemClient } from '../zmodem-client.js'
16
+ import { TrzszClient } from '../trzsz-client.js'
17
+ import { XmodemClient } from '../xmodem-client.js'
18
+ import { loadWebLinksAddon } from '../xterm-loader.js'
19
+ import { KeywordHighlighterAddon } from '../highlight-addon.js'
20
+ import {
21
+ createSshReloadState,
22
+ getAlternateBufferSnapshot,
23
+ shouldCaptureSshReloadState
24
+ } from '../ssh-reload-state.js'
25
+
26
+ // Expands \n \t \r \\ and \xHH hex byte escapes, used to let users type
27
+ // control bytes (e.g. \x01 = Ctrl+A) in the serial "closeSequence" field.
28
+ function expandCloseSequence (text) {
29
+ let result = ''
30
+ for (let i = 0; i < text.length; i++) {
31
+ if (text[i] === '\\' && i + 1 < text.length) {
32
+ const next = text[i + 1]
33
+ if (next === 'n') {
34
+ result += '\n'
35
+ i++
36
+ } else if (next === 't') {
37
+ result += '\t'
38
+ i++
39
+ } else if (next === 'r') {
40
+ result += '\r'
41
+ i++
42
+ } else if (next === '\\') {
43
+ result += '\\'
44
+ i++
45
+ } else if (next === 'x' && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 2, i + 4))) {
46
+ result += String.fromCharCode(parseInt(text.slice(i + 2, i + 4), 16))
47
+ i += 3
48
+ } else {
49
+ result += text[i]
50
+ }
51
+ } else {
52
+ result += text[i]
53
+ }
54
+ }
55
+ return result
56
+ }
57
+
58
+ /**
59
+ * Session lifecycle: opening the pty/ssh session over its websocket, error
60
+ * and reconnect handling, input broadcast, and the screen snapshot used to
61
+ * restore an ssh session after a reload.
62
+ */
63
+ export const socketMixin = {
64
+ buildWsUrl (port) {
65
+ const { host, tokenElecterm } = this.props.config
66
+ const { id } = this.props.tab
67
+ if (window.et.buildWsUrl) {
68
+ return window.et.buildWsUrl(
69
+ host,
70
+ port,
71
+ tokenElecterm,
72
+ id
73
+ )
74
+ }
75
+ return `ws://${host}:${port}/terminals/${id}?token=${tokenElecterm}`
76
+ },
77
+
78
+ async remoteInit (term = this.term) {
79
+ this.setState({
80
+ loading: true,
81
+ terminalError: null
82
+ })
83
+ const { cols, rows } = term
84
+ const { config } = this.props
85
+ const {
86
+ keywords = []
87
+ } = config
88
+ const { logName } = this.props
89
+ const tab = window.store.applyProfileToTabs(deepCopy(this.props.tab || {}))
90
+ const {
91
+ srcId, from = 'bookmarks',
92
+ type,
93
+ term: terminalType,
94
+ displayRaw,
95
+ id
96
+ } = tab
97
+ const { savePassword } = this.state
98
+ const termType = type
99
+ const extra = this.props.sessionOptions
100
+ // Determine if this is a local terminal (no host)
101
+ const isLocalType = !tab.host
102
+ // Build exec settings: only for local type, prefer tab settings over config
103
+ let execOpts = {}
104
+ let execPropName = 'execLinux'
105
+ if (isWin) {
106
+ execPropName = 'execWindows'
107
+ } else if (isMac) {
108
+ execPropName = 'execMac'
109
+ }
110
+ if (isLocalType) {
111
+ // Check flat properties on tab first (bookmark data), then fall back to config
112
+ if (tab[execPropName]) {
113
+ // Use bookmark's exec setting directly
114
+ execOpts = {
115
+ [execPropName]: tab[execPropName],
116
+ [`${execPropName}Args`]: tab[`${execPropName}Args`] || []
117
+ }
118
+ } else if (config[execPropName]) {
119
+ // Use global config exec settings
120
+ execOpts = {
121
+ [execPropName]: config[execPropName],
122
+ [`${execPropName}Args`]: config[`${execPropName}Args`] || []
123
+ }
124
+ }
125
+ }
126
+ const keepaliveInterval = tab.keepaliveInterval || config.keepaliveInterval
127
+ const opts = clone({
128
+ cols,
129
+ rows,
130
+ term: terminalType || config.terminalType,
131
+ saveTerminalLogToFile: config.saveTerminalLogToFile,
132
+ ...tab,
133
+ ...extra,
134
+ ...execOpts,
135
+ logName,
136
+ sessionLogPath: this.state.logPath,
137
+ ...pick(config, [
138
+ 'addTimeStampToTermLog',
139
+ 'keepaliveCountMax',
140
+ 'keyword2FA',
141
+ 'debug'
142
+ ]),
143
+ keepaliveInterval,
144
+ tabId: id,
145
+ uid: id,
146
+ srcTabId: tab.id,
147
+ termType,
148
+ readyTimeout: config.sshReadyTimeout,
149
+ proxy: getProxy(tab, config),
150
+ type: tab.host
151
+ ? typeMap.remote
152
+ : typeMap.local
153
+ })
154
+ // Renderer-only state can contain a large serialized screen and must not
155
+ // cross the process boundary as part of the SSH connection options.
156
+ delete opts._reloadState
157
+ const isAutoReconnect = !!(tab.autoReConnect && this.props.config.autoReconnectTerminal)
158
+ const r = await createTerm(opts)
159
+ .catch(err => {
160
+ if (!isAutoReconnect) {
161
+ const text = err.message
162
+ this.handleError({ message: text, from, srcId })
163
+ }
164
+ })
165
+ // Guard: component was unmounted while createTerm was pending.
166
+ // The child process is already running; connect briefly to trigger its cleanup.
167
+ if (this.onClose) {
168
+ if (r && r.port) {
169
+ try {
170
+ const tmpSock = new WebSocket(this.buildWsUrl(r.port))
171
+ tmpSock.onopen = () => tmpSock.close()
172
+ } catch (_e) {}
173
+ }
174
+ return
175
+ }
176
+ if (typeof r === 'string' && r.includes('fail')) {
177
+ return this.promote()
178
+ }
179
+ if (savePassword) {
180
+ window.store.editItem(srcId, extra, from)
181
+ }
182
+ this.setState({
183
+ loading: false
184
+ })
185
+ if (!r) {
186
+ if (isAutoReconnect) {
187
+ this.scheduleAutoReconnect(3000)
188
+ return
189
+ }
190
+ this.setStatus(statusMap.error)
191
+ return
192
+ }
193
+ this.port = r.port
194
+ this.setStatus(statusMap.success)
195
+ refs.get('sftp-' + id)?.initData(id, r.port)
196
+ term.pid = id
197
+ this.pid = id
198
+ const wsUrl = this.buildWsUrl(r.port)
199
+ const socket = new WebSocket(wsUrl)
200
+ socket.onclose = this.oncloseSocket
201
+ socket.onerror = this.onerrorSocket
202
+ this.socket = socket
203
+ this.initSocketEvents()
204
+ this.term = term
205
+ socket.onopen = async () => {
206
+ // attach addon is a dynamic import too — report a failed chunk fetch
207
+ // instead of leaving an unhandled rejection behind
208
+ try {
209
+ await this.initAttachAddon()
210
+ this.startupQueue.runInitScript()
211
+ } catch (e) {
212
+ console.error(e)
213
+ this.handleError({ message: e.message })
214
+ }
215
+ }
216
+ // term.onRrefresh(this.onRefresh)
217
+ term.onResize(this.onResizeTerminal)
218
+ // xterm 6.x exposes buffer change as a public event (IBufferNamespace.onBufferChange).
219
+ // Previously this reached into the private _onBufferChange._listeners array.
220
+ term.buffer.onBufferChange(this.onBufferChange)
221
+ const WebLinksAddon = await loadWebLinksAddon()
222
+ term.loadAddon(new WebLinksAddon(this.webLinkHandler))
223
+ term.focus()
224
+ this.zmodemClient = new ZmodemClient(this)
225
+ this.zmodemClient.init(socket)
226
+ this.trzszClient = new TrzszClient(this)
227
+ this.trzszClient.init(socket)
228
+ this.xmodemClient = new XmodemClient(this)
229
+ this.xmodemClient.init(socket)
230
+ // 仅在可见时 fit,隐藏标签跳过,避免 0 列损坏提示符。
231
+ if (this.isElementVisible()) {
232
+ this.fitAddon.fit()
233
+ }
234
+ // Force-send the current size now that `this.pid` is finally valid.
235
+ // A resize may already have fired earlier (e.g. from the tab-becomes-
236
+ // visible fit in componentDidUpdate, which can run before createTerm()
237
+ // resolves) while this.pid was still undefined - the backend silently
238
+ // drops resize calls for an unknown pid. If that earlier call already
239
+ // updated term.cols/rows locally, xterm's resize() no-ops on the next
240
+ // fit() (dims unchanged) and never fires onResize again, so the
241
+ // backend pty would otherwise be stuck at its initial (possibly wrong)
242
+ // size forever. Sending explicitly here, keyed off the real pid,
243
+ // guarantees the remote pty/ssh channel is told the true current size.
244
+ resizeTerm(this.pid, term.cols, term.rows)
245
+ term.displayRaw = displayRaw
246
+ term.loadAddon(
247
+ new KeywordHighlighterAddon(keywords)
248
+ )
249
+ },
250
+
251
+ handleError ({ message: errorMessage, from, srcId }) {
252
+ this.setState({
253
+ terminalError: {
254
+ message: errorMessage || 'Failed to create terminal session',
255
+ from,
256
+ srcId
257
+ }
258
+ })
259
+ },
260
+
261
+ handleEditBookmarkFromError () {
262
+ const error = this.state.terminalError
263
+ if (!error || error.from !== 'bookmarks' || !error.srcId) {
264
+ return
265
+ }
266
+ const item = window.store.bookmarksMap?.get(error.srcId) ||
267
+ window.store.bookmarks?.find(d => d.id === error.srcId)
268
+ if (!item) {
269
+ return
270
+ }
271
+ window.store.openBookmarkEdit(item)
272
+ },
273
+
274
+ initSocketEvents () {
275
+ const originalSend = this.socket.send
276
+ this.socket.send = (data) => {
277
+ // Call original send first
278
+ originalSend.call(this.socket, data)
279
+
280
+ // Broadcast to other terminals
281
+ this.broadcastSocketData(data)
282
+ }
283
+ },
284
+
285
+ canReceiveBroadcast (termRef) {
286
+ return (
287
+ termRef.socket &&
288
+ termRef.props?.tab.pane === paneMap.terminal
289
+ )
290
+ },
291
+
292
+ broadcastSocketData (data) {
293
+ if (!this.isActiveTerminal() || !this.props.broadcastInput) {
294
+ return
295
+ }
296
+
297
+ window.refs.forEach((termRef, refId) => {
298
+ if (
299
+ refId !== this.id &&
300
+ refId.startsWith('term-') &&
301
+ this.canReceiveBroadcast(termRef)
302
+ ) {
303
+ termRef.socket.send(data)
304
+ }
305
+ })
306
+ },
307
+
308
+ onerrorSocket (err) {
309
+ console.error('onerrorSocket', err)
310
+ },
311
+
312
+ oncloseSocket () {
313
+ if (this.onClose || this.props.tab.enableSsh === false) {
314
+ return
315
+ }
316
+ this.setStatus(
317
+ statusMap.error
318
+ )
319
+ if (this.userTypeExit) {
320
+ return this.props.delTab(this.props.tab.id)
321
+ }
322
+ const { autoReconnectTerminal } = this.props.config
323
+ if (autoReconnectTerminal) {
324
+ this.scheduleAutoReconnect(3000)
325
+ }
326
+ },
327
+
328
+ scheduleAutoReconnect (delay = 3000) {
329
+ clearTimeout(this.timers.reconnectTimer)
330
+ clearInterval(this.timers.reconnectCountdown)
331
+ const seconds = Math.round(delay / 1000)
332
+ this.setState({ reconnectCountdown: seconds })
333
+ let remaining = seconds
334
+ this.timers.reconnectCountdown = setInterval(() => {
335
+ remaining -= 1
336
+ if (remaining <= 0) {
337
+ clearInterval(this.timers.reconnectCountdown)
338
+ this.timers.reconnectCountdown = null
339
+ }
340
+ this.setState({ reconnectCountdown: remaining <= 0 ? null : remaining })
341
+ }, 1000)
342
+ this.timers.reconnectTimer = setTimeout(() => {
343
+ clearInterval(this.timers.reconnectCountdown)
344
+ this.timers.reconnectCountdown = null
345
+ this.setState({ reconnectCountdown: null })
346
+ if (this.onClose || !this.props.config.autoReconnectTerminal) {
347
+ return
348
+ }
349
+ const reconnectCount = (this.props.tab.autoReConnect || 0) + 1
350
+ this.props.reloadTab({ ...this.props.tab, autoReConnect: reconnectCount })
351
+ }, delay)
352
+ },
353
+
354
+ handleCancelAutoReconnect () {
355
+ clearTimeout(this.timers.reconnectTimer)
356
+ clearInterval(this.timers.reconnectCountdown)
357
+ this.timers.reconnectTimer = null
358
+ this.timers.reconnectCountdown = null
359
+ this.setState({ reconnectCountdown: null })
360
+ },
361
+
362
+ handleCancel () {
363
+ const { id } = this.props.tab
364
+ this.props.delTab(id)
365
+ },
366
+
367
+ /**
368
+ * Manually triggered from the "exit gracefully" control in
369
+ * session-control.jsx (serial tabs only). Writes the configured key
370
+ * sequence (default \x01ky = Ctrl+A, k, y to kill a GNU screen window) to
371
+ * the socket, waits a bit so the remote end (e.g. a Bluetooth console
372
+ * adapter) can release cleanly, then closes the tab.
373
+ */
374
+ async exitGracefully () {
375
+ const { tab } = this.props
376
+ if (tab.type !== terminalSerialType) {
377
+ return
378
+ }
379
+ if (!this.onClose && this.attachAddon?._sendData) {
380
+ try {
381
+ const data = expandCloseSequence(tab.closeSequence || '\\x01ky')
382
+ if (data) {
383
+ this.attachAddon._sendData(data)
384
+ }
385
+ } catch (err) {
386
+ console.error('send close sequence failed', err)
387
+ }
388
+ const delay = Number(tab.closeSequenceDelay)
389
+ await new Promise(resolve => setTimeout(resolve, Number.isFinite(delay) && delay >= 0 ? delay : 500))
390
+ }
391
+ this.props.delTab(tab.id)
392
+ },
393
+
394
+ getReloadState () {
395
+ if (!shouldCaptureSshReloadState(this.props.tab, this.props.config)) {
396
+ return undefined
397
+ }
398
+ let screen = ''
399
+ try {
400
+ screen = this.serializeAddon?.serialize({
401
+ scrollback: this.props.config.scrollback,
402
+ excludeAltBuffer: true,
403
+ excludeModes: true
404
+ }) || ''
405
+ // Full-screen programs use the alternate buffer. Replaying that buffer
406
+ // as a terminal mode would trap the fresh shell in stale mouse/cursor
407
+ // state, so preserve its visible text as a safe normal-buffer snapshot.
408
+ const alternateScreen = getAlternateBufferSnapshot(this.term?.buffer.active)
409
+ if (alternateScreen) {
410
+ screen += `${screen ? '\r\n' : ''}${alternateScreen}`
411
+ }
412
+ } catch (e) {
413
+ console.warn('Failed to serialize terminal before reload', e)
414
+ }
415
+ const cwd = this.cmdAddon?.getCwd() || this.props.tab._reloadState?.cwd || ''
416
+ return createSshReloadState({ cwd, screen })
417
+ },
418
+
419
+ restoreReloadScreen (term) {
420
+ const screen = this.props.tab._reloadState?.screen
421
+ if (!screen) {
422
+ return Promise.resolve()
423
+ }
424
+ return new Promise(resolve => {
425
+ term.write(screen, () => {
426
+ // The framebuffer has been replayed; do not keep a potentially large
427
+ // duplicate string on the live tab. A later reload serializes afresh.
428
+ delete this.props.tab._reloadState.screen
429
+ resolve()
430
+ })
431
+ })
432
+ }
433
+ }