@electerm/electerm-react 3.11.12 → 3.12.0

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.
@@ -96,7 +96,8 @@ export const serialBookmarkSchema = {
96
96
  xon: z.boolean().optional().describe('XON flow control'),
97
97
  xoff: z.boolean().optional().describe('XOFF flow control'),
98
98
  xany: z.boolean().optional().describe('XANY flow control'),
99
- lineEnding: z.enum(['', '\r', '\n', '\r\n']).optional().describe('Line ending for Enter key: "" (none), "\\r" (CR), "\\n" (LF), "\\r\\n" (CR+LF)'),
99
+ txLineEnding: z.enum(['\r', '\n', '\r\n']).optional().describe('TX line ending appended on Enter: "\\r" (CR, default), "\\n" (LF), "\\r\\n" (CR+LF)'),
100
+ rxLineEnding: z.enum(['none', 'lf_to_crlf', 'cr_to_crlf']).optional().describe('RX line ending conversion: "none" (pass-through, default), "lf_to_crlf" (LF→CRLF for LF-only devices), "cr_to_crlf" (CR→CRLF for CR-only devices)'),
100
101
  description: z.string().optional().describe('Bookmark description')
101
102
  }
102
103
 
@@ -180,13 +180,21 @@ export const commonParities = [
180
180
  'none', 'even', 'mark', 'odd', 'space'
181
181
  ]
182
182
 
183
- export const commonLineEndings = [
184
- { value: '', label: 'none' },
183
+ export const commonTxLineEndings = [
185
184
  { value: '\r', label: 'CR' },
186
185
  { value: '\n', label: 'LF' },
187
186
  { value: '\r\n', label: 'CR+LF' }
188
187
  ]
189
188
 
189
+ export const commonRxLineEndings = [
190
+ { value: 'none', label: 'None' },
191
+ { value: 'lf_to_crlf', label: 'LF→CRLF' },
192
+ { value: 'cr_to_crlf', label: 'CR→CRLF' }
193
+ ]
194
+
195
+ // backward compat alias
196
+ export const commonLineEndings = commonTxLineEndings
197
+
190
198
  export const maxBatchInput = 30
191
199
  export const windowControlWidth = 94
192
200
  export const baseUpdateCheckUrls = [
@@ -248,6 +256,7 @@ export const proxyHelpLink = 'https://github.com/electerm/electerm/wiki/proxy-fo
248
256
  export const regexHelpLink = 'https://github.com/electerm/electerm/wiki/Terminal-keywords-highlight-regular-expression-exmaples'
249
257
  export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/wiki/Connection-Hopping-Behavior-Change-in-electerm-since-v1.50.65'
250
258
  export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
259
+ export const aiChatModeLsKey = 'ai-chat-mode'
251
260
  export const modals = {
252
261
  hide: 0,
253
262
  setting: 1
@@ -118,6 +118,23 @@ export const agentTools = [
118
118
  }
119
119
  }
120
120
  },
121
+ {
122
+ type: 'function',
123
+ function: {
124
+ name: 'close_tab',
125
+ description: 'Close a terminal tab by its ID. Use this to clean up tabs after a task is finished.',
126
+ parameters: {
127
+ type: 'object',
128
+ properties: {
129
+ tabId: {
130
+ type: 'string',
131
+ description: 'The tab ID to close.'
132
+ }
133
+ },
134
+ required: ['tabId']
135
+ }
136
+ }
137
+ },
121
138
  {
122
139
  type: 'function',
123
140
  function: {
@@ -153,6 +170,170 @@ export const agentTools = [
153
170
  description: 'Create a new bookmark. Specify the type and provide type-specific fields. Supported types: ' + Object.keys(bookmarkSchemas).join(', ') + '.',
154
171
  parameters: buildAddBookmarkParameters()
155
172
  }
173
+ },
174
+ {
175
+ type: 'function',
176
+ function: {
177
+ name: 'open_tab',
178
+ description: 'Open a terminal tab directly with connection parameters without creating a bookmark. Supported types: ' + Object.keys(bookmarkSchemas).join(', ') + '.',
179
+ parameters: buildAddBookmarkParameters()
180
+ }
181
+ },
182
+ {
183
+ type: 'function',
184
+ function: {
185
+ name: 'sftp_list',
186
+ description: 'List files and directories at a remote path via SFTP. Requires an SSH/FTP tab.',
187
+ parameters: {
188
+ type: 'object',
189
+ properties: {
190
+ remotePath: {
191
+ type: 'string',
192
+ description: 'Remote directory path to list.'
193
+ },
194
+ tabId: {
195
+ type: 'string',
196
+ description: 'SSH/FTP tab ID. Omit to use the active tab.'
197
+ }
198
+ },
199
+ required: ['remotePath']
200
+ }
201
+ }
202
+ },
203
+ {
204
+ type: 'function',
205
+ function: {
206
+ name: 'sftp_stat',
207
+ description: 'Get file/directory stats (size, permissions, etc.) at a remote path via SFTP.',
208
+ parameters: {
209
+ type: 'object',
210
+ properties: {
211
+ remotePath: {
212
+ type: 'string',
213
+ description: 'Remote path to stat.'
214
+ },
215
+ tabId: {
216
+ type: 'string',
217
+ description: 'SSH/FTP tab ID. Omit to use the active tab.'
218
+ }
219
+ },
220
+ required: ['remotePath']
221
+ }
222
+ }
223
+ },
224
+ {
225
+ type: 'function',
226
+ function: {
227
+ name: 'sftp_read_file',
228
+ description: 'Read the contents of a remote file via SFTP.',
229
+ parameters: {
230
+ type: 'object',
231
+ properties: {
232
+ remotePath: {
233
+ type: 'string',
234
+ description: 'Remote file path to read.'
235
+ },
236
+ tabId: {
237
+ type: 'string',
238
+ description: 'SSH/FTP tab ID. Omit to use the active tab.'
239
+ }
240
+ },
241
+ required: ['remotePath']
242
+ }
243
+ }
244
+ },
245
+ {
246
+ type: 'function',
247
+ function: {
248
+ name: 'sftp_del',
249
+ description: 'Delete a remote file or directory via SFTP.',
250
+ parameters: {
251
+ type: 'object',
252
+ properties: {
253
+ remotePath: {
254
+ type: 'string',
255
+ description: 'Remote file or directory path to delete.'
256
+ },
257
+ tabId: {
258
+ type: 'string',
259
+ description: 'SSH/FTP tab ID. Omit to use the active tab.'
260
+ }
261
+ },
262
+ required: ['remotePath']
263
+ }
264
+ }
265
+ },
266
+ {
267
+ type: 'function',
268
+ function: {
269
+ name: 'sftp_upload',
270
+ description: 'Upload a local file to a remote server via SFTP.',
271
+ parameters: {
272
+ type: 'object',
273
+ properties: {
274
+ localPath: {
275
+ type: 'string',
276
+ description: 'Local file path to upload.'
277
+ },
278
+ remotePath: {
279
+ type: 'string',
280
+ description: 'Remote destination path.'
281
+ },
282
+ tabId: {
283
+ type: 'string',
284
+ description: 'SSH/FTP tab ID. Omit to use the active tab.'
285
+ }
286
+ },
287
+ required: ['localPath', 'remotePath']
288
+ }
289
+ }
290
+ },
291
+ {
292
+ type: 'function',
293
+ function: {
294
+ name: 'sftp_download',
295
+ description: 'Download a remote file to a local path via SFTP.',
296
+ parameters: {
297
+ type: 'object',
298
+ properties: {
299
+ remotePath: {
300
+ type: 'string',
301
+ description: 'Remote file path to download.'
302
+ },
303
+ localPath: {
304
+ type: 'string',
305
+ description: 'Local destination path.'
306
+ },
307
+ tabId: {
308
+ type: 'string',
309
+ description: 'SSH/FTP tab ID. Omit to use the active tab.'
310
+ }
311
+ },
312
+ required: ['remotePath', 'localPath']
313
+ }
314
+ }
315
+ },
316
+ {
317
+ type: 'function',
318
+ function: {
319
+ name: 'sftp_transfer_list',
320
+ description: 'List current active SFTP file transfers.',
321
+ parameters: {
322
+ type: 'object',
323
+ properties: {}
324
+ }
325
+ }
326
+ },
327
+ {
328
+ type: 'function',
329
+ function: {
330
+ name: 'sftp_transfer_history',
331
+ description: 'List past SFTP file transfer history.',
332
+ parameters: {
333
+ type: 'object',
334
+ properties: {}
335
+ }
336
+ }
156
337
  }
157
338
  ]
158
339
 
@@ -178,6 +359,8 @@ export async function executeToolCall (toolName, args) {
178
359
  return JSON.stringify(store.mcpGetActiveTab())
179
360
  case 'switch_tab':
180
361
  return JSON.stringify(store.mcpSwitchTab(args))
362
+ case 'close_tab':
363
+ return JSON.stringify(store.mcpCloseTab(args))
181
364
  case 'list_bookmarks':
182
365
  return JSON.stringify(store.mcpListBookmarks())
183
366
  case 'open_bookmark':
@@ -187,6 +370,27 @@ export async function executeToolCall (toolName, args) {
187
370
  const typeFields = args[type] || {}
188
371
  return JSON.stringify(await store.mcpAddBookmark({ type, ...typeFields }))
189
372
  }
373
+ case 'open_tab': {
374
+ const { type } = args
375
+ const typeFields = args[type] || {}
376
+ return JSON.stringify(store.mcpOpenTab({ type, ...typeFields }))
377
+ }
378
+ case 'sftp_list':
379
+ return JSON.stringify(await store.mcpSftpList(args))
380
+ case 'sftp_stat':
381
+ return JSON.stringify(await store.mcpSftpStat(args))
382
+ case 'sftp_read_file':
383
+ return JSON.stringify(await store.mcpSftpReadFile(args))
384
+ case 'sftp_del':
385
+ return JSON.stringify(await store.mcpSftpDel(args))
386
+ case 'sftp_upload':
387
+ return JSON.stringify(await store.mcpSftpUpload(args))
388
+ case 'sftp_download':
389
+ return JSON.stringify(await store.mcpSftpDownload(args))
390
+ case 'sftp_transfer_list':
391
+ return JSON.stringify(store.mcpSftpTransferList())
392
+ case 'sftp_transfer_history':
393
+ return JSON.stringify(store.mcpSftpTransferHistory())
190
394
  default:
191
395
  throw new Error(`Unknown agent tool: ${toolName}`)
192
396
  }
@@ -1,6 +1,6 @@
1
1
  import { agentTools, executeToolCall } from './agent-tools'
2
2
 
3
- const MAX_ITERATIONS = 15
3
+ const MAX_ITERATIONS = 150
4
4
 
5
5
  function buildAgentSystemPrompt (config) {
6
6
  const lang = config.languageAI || window.store.getLangName()
@@ -12,12 +12,14 @@ You are operating inside electerm, a terminal/SSH client. You have access to too
12
12
  - Open new terminal tabs (local or SSH)
13
13
  - Manage bookmarks (create, list, open connections)
14
14
  - Switch between tabs
15
+ - Transfer files via SFTP (upload, download, list, read, delete remote files)
15
16
 
16
17
  When the user asks you to perform terminal operations, use the available tools.
17
18
  Always explain what you are doing before executing commands.
18
19
  If a command produces errors, analyze the output and try to fix the issue.
19
20
  Prefer using the active terminal unless the user specifies otherwise.
20
- For SSH connections, create a bookmark and open it rather than running ssh directly.
21
+ For SSH connections, prefer using open_tab to connect directly, or create a bookmark with add_bookmark and open it with open_bookmark if the user wants to save the connection.
22
+ For file transfers, use the sftp_upload and sftp_download tools. The tab must be an SSH/FTP connection with SFTP initialized.
21
23
 
22
24
  Reply in ${lang} language.`
23
25
  }
@@ -43,7 +45,7 @@ async function callBackendAIchatWithTools (messages, config) {
43
45
  )
44
46
  }
45
47
 
46
- export async function runAgentLoop (chatEntry, config, abortRef) {
48
+ export async function runAgentLoop (chatEntry, config, abortRef, setIsStreaming) {
47
49
  const messages = [
48
50
  { role: 'system', content: buildAgentSystemPrompt(config) },
49
51
  { role: 'user', content: chatEntry.prompt }
@@ -51,16 +53,16 @@ export async function runAgentLoop (chatEntry, config, abortRef) {
51
53
  const toolCallsLog = []
52
54
  let accumulatedContent = ''
53
55
 
56
+ setIsStreaming(true)
54
57
  updateChatEntry(chatEntry, {
55
- isStreaming: true,
56
58
  toolCalls: [],
57
59
  response: ''
58
60
  })
59
61
 
60
62
  for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
61
63
  if (abortRef && abortRef.current) {
64
+ setIsStreaming(false)
62
65
  updateChatEntry(chatEntry, {
63
- isStreaming: false,
64
66
  response: accumulatedContent + '\n\n*(Agent stopped by user)*'
65
67
  })
66
68
  return
@@ -69,8 +71,8 @@ export async function runAgentLoop (chatEntry, config, abortRef) {
69
71
  const result = await callBackendAIchatWithTools(messages, config)
70
72
 
71
73
  if (result.error) {
74
+ setIsStreaming(false)
72
75
  updateChatEntry(chatEntry, {
73
- isStreaming: false,
74
76
  response: accumulatedContent + `\n\n**Error:** ${result.error}`
75
77
  })
76
78
  return
@@ -78,8 +80,8 @@ export async function runAgentLoop (chatEntry, config, abortRef) {
78
80
 
79
81
  const assistantMessage = result.message
80
82
  if (!assistantMessage) {
83
+ setIsStreaming(false)
81
84
  updateChatEntry(chatEntry, {
82
- isStreaming: false,
83
85
  response: accumulatedContent || 'No response from AI.'
84
86
  })
85
87
  return
@@ -95,8 +97,8 @@ export async function runAgentLoop (chatEntry, config, abortRef) {
95
97
  }
96
98
 
97
99
  if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
100
+ setIsStreaming(false)
98
101
  updateChatEntry(chatEntry, {
99
- isStreaming: false,
100
102
  response: accumulatedContent
101
103
  })
102
104
  return
@@ -104,8 +106,8 @@ export async function runAgentLoop (chatEntry, config, abortRef) {
104
106
 
105
107
  for (const toolCall of assistantMessage.tool_calls) {
106
108
  if (abortRef && abortRef.current) {
109
+ setIsStreaming(false)
107
110
  updateChatEntry(chatEntry, {
108
- isStreaming: false,
109
111
  response: accumulatedContent + '\n\n*(Agent stopped by user)*'
110
112
  })
111
113
  return
@@ -152,8 +154,8 @@ export async function runAgentLoop (chatEntry, config, abortRef) {
152
154
  }
153
155
  }
154
156
 
157
+ setIsStreaming(false)
155
158
  updateChatEntry(chatEntry, {
156
- isStreaming: false,
157
159
  response: accumulatedContent + '\n\n*(Agent reached maximum iterations)*'
158
160
  })
159
161
  }
@@ -18,13 +18,11 @@ import { copy } from '../../common/clipboard'
18
18
 
19
19
  export default function AIChatHistoryItem ({ item }) {
20
20
  const [showOutput, setShowOutput] = useState(true)
21
- const startedRef = useRef(false)
21
+ const [isStreaming, setIsStreaming] = useState(false)
22
22
  const abortRef = useRef(false)
23
23
  const {
24
24
  prompt,
25
- isStreaming,
26
25
  sessionId,
27
- response,
28
26
  modelAI,
29
27
  roleAI,
30
28
  baseURLAI,
@@ -60,12 +58,11 @@ export default function AIChatHistoryItem ({ item }) {
60
58
  const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
61
59
  if (index !== -1) {
62
60
  window.store.aiChatHistory[index].response = streamResponse.content || ''
63
- window.store.aiChatHistory[index].isStreaming = streamResponse.hasMore
64
61
  window.store.aiChatHistory = [...window.store.aiChatHistory]
65
-
66
- if (streamResponse.hasMore) {
67
- setTimeout(() => pollStreamContent(sid), 200)
68
- }
62
+ }
63
+ setIsStreaming(streamResponse.hasMore)
64
+ if (streamResponse.hasMore) {
65
+ setTimeout(() => pollStreamContent(sid), 200)
69
66
  }
70
67
  } catch (error) {
71
68
  window.store.removeAiHistory(item.id)
@@ -93,9 +90,9 @@ export default function AIChatHistoryItem ({ item }) {
93
90
  }
94
91
 
95
92
  if (aiResponse && aiResponse.isStream && aiResponse.sessionId) {
93
+ setIsStreaming(true)
96
94
  const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
97
95
  if (index !== -1) {
98
- window.store.aiChatHistory[index].isStreaming = true
99
96
  window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
100
97
  window.store.aiChatHistory[index].response = aiResponse.content || ''
101
98
  }
@@ -104,7 +101,6 @@ export default function AIChatHistoryItem ({ item }) {
104
101
  const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
105
102
  if (index !== -1) {
106
103
  window.store.aiChatHistory[index].response = aiResponse.response
107
- window.store.aiChatHistory[index].isStreaming = false
108
104
  }
109
105
  }
110
106
  } catch (error) {
@@ -124,12 +120,15 @@ export default function AIChatHistoryItem ({ item }) {
124
120
  proxyAI,
125
121
  languageAI
126
122
  }
127
- await runAgentLoop(item, config, abortRef)
123
+ await runAgentLoop(item, config, abortRef, setIsStreaming)
128
124
  }, [modelAI, roleAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, languageAI, item.id])
129
125
 
130
126
  useEffect(() => {
131
- if (!response && !startedRef.current) {
132
- startedRef.current = true
127
+ if (item.pending) {
128
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
129
+ if (index !== -1) {
130
+ window.store.aiChatHistory[index].pending = false
131
+ }
133
132
  if (mode === 'agent') {
134
133
  startAgentRequest()
135
134
  } else {
@@ -142,22 +141,14 @@ export default function AIChatHistoryItem ({ item }) {
142
141
  e.stopPropagation()
143
142
  if (mode === 'agent') {
144
143
  abortRef.current = true
145
- const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
146
- if (index !== -1) {
147
- window.store.aiChatHistory[index].isStreaming = false
148
- window.store.aiChatHistory = [...window.store.aiChatHistory]
149
- }
144
+ setIsStreaming(false)
150
145
  return
151
146
  }
152
147
  if (!sessionId) return
153
148
 
154
149
  try {
155
150
  await window.pre.runGlobalAsync('stopStream', sessionId)
156
- const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
157
- if (index !== -1) {
158
- window.store.aiChatHistory[index].isStreaming = false
159
- window.store.aiChatHistory = [...window.store.aiChatHistory]
160
- }
151
+ setIsStreaming(false)
161
152
  } catch (error) {
162
153
  console.error('Error stopping stream:', error)
163
154
  }
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useEffect } from 'react'
2
- import { Flex, Input, Segmented } from 'antd'
2
+ import { Flex, Input, Popconfirm, Segmented } from 'antd'
3
3
  import TabSelect from '../footer/tab-select'
4
4
  import AiChatHistory from './ai-chat-history'
5
5
  import uid from '../../common/uid'
@@ -10,8 +10,10 @@ import {
10
10
  UnorderedListOutlined
11
11
  } from '@ant-design/icons'
12
12
  import {
13
- aiConfigWikiLink
13
+ aiConfigWikiLink,
14
+ aiChatModeLsKey
14
15
  } from '../../common/constants'
16
+ import { getItem, setItem } from '../../common/safe-local-storage.js'
15
17
  import HelpIcon from '../common/help-icon'
16
18
  import { refsStatic } from '../common/ref'
17
19
  import './ai.styl'
@@ -21,13 +23,19 @@ const MAX_HISTORY = 100
21
23
 
22
24
  export default function AIChat (props) {
23
25
  const [prompt, setPrompt] = useState('')
24
- const [mode, setMode] = useState('ask')
26
+ const [mode, setMode] = useState(() => getItem(aiChatModeLsKey) || 'ask')
25
27
  const isAgent = mode === 'agent'
26
28
 
27
29
  function handlePromptChange (e) {
28
30
  setPrompt(e.target.value)
29
31
  }
30
32
 
33
+ function handleModeChange (val) {
34
+ const m = val === 'Ask' ? 'ask' : 'agent'
35
+ setItem(aiChatModeLsKey, m)
36
+ setMode(m)
37
+ }
38
+
31
39
  const handleSubmit = useCallback(function () {
32
40
  if (window.store.aiConfigMissing()) {
33
41
  window.store.toggleAIConfig()
@@ -39,6 +47,7 @@ export default function AIChat (props) {
39
47
  prompt,
40
48
  response: '',
41
49
  isStreaming: false,
50
+ pending: true,
42
51
  sessionId: null,
43
52
  mode,
44
53
  toolCalls: [],
@@ -146,7 +155,7 @@ export default function AIChat (props) {
146
155
  <Segmented
147
156
  options={['Ask', 'Agent']}
148
157
  value={mode === 'ask' ? 'Ask' : 'Agent'}
149
- onChange={(val) => setMode(val === 'Ask' ? 'ask' : 'agent')}
158
+ onChange={handleModeChange}
150
159
  size='small'
151
160
  />
152
161
  {renderTabSelect()}
@@ -154,11 +163,17 @@ export default function AIChat (props) {
154
163
  onClick={toggleConfig}
155
164
  className='mg1l pointer icon-hover toggle-ai-setting-icon'
156
165
  />
157
- <UnorderedListOutlined
158
- onClick={clearHistory}
159
- className='mg2x pointer clear-ai-icon icon-hover'
160
- title='Clear AI chat history'
161
- />
166
+ <Popconfirm
167
+ title={window.translate('clear') + ' AI ' + window.translate('history') + '?'}
168
+ okText={window.translate('ok')}
169
+ cancelText={window.translate('cancel')}
170
+ onConfirm={clearHistory}
171
+ >
172
+ <UnorderedListOutlined
173
+ className='mg2x pointer clear-ai-icon icon-hover'
174
+ title='Clear AI chat history'
175
+ />
176
+ </Popconfirm>
162
177
  <HelpIcon
163
178
  link={aiConfigWikiLink}
164
179
  />
@@ -81,7 +81,8 @@ const bookmarkSchema = {
81
81
  xon: 'boolean - enable XON flow control, default is false',
82
82
  xoff: 'boolean - enable XOFF flow control, default is false',
83
83
  xany: 'boolean - enable XANY flow control, default is false',
84
- lineEnding: 'string - line ending for Enter key: "" (none), "\\r" (CR), "\\n" (LF), "\\r\\n" (CR+LF)',
84
+ txLineEnding: 'string - TX line ending on Enter: "\\r" (CR, default), "\\n" (LF), "\\r\\n" (CR+LF)',
85
+ rxLineEnding: 'string - RX line ending conversion: "none" (default), "lf_to_crlf" (for LF-only devices), "cr_to_crlf" (for CR-only devices)',
85
86
  runScripts: 'array - run scripts after connected ({delay,script})',
86
87
  description: 'string - bookmark description'
87
88
  },
@@ -1,5 +1,5 @@
1
1
  import { formItemLayout } from '../../../common/form-layout.js'
2
- import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities, commonLineEndings } from '../../../common/constants.js'
2
+ import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities, commonTxLineEndings, commonRxLineEndings } from '../../../common/constants.js'
3
3
  import defaultSettings from '../../../common/default-setting.js'
4
4
  import { createBaseInitValues, getTerminalBackgroundDefaults } from '../common/init-values.js'
5
5
  import { commonFields } from './common-fields.js'
@@ -57,7 +57,8 @@ const serialConfig = {
57
57
  { type: 'switch', name: 'xon', label: 'xon', valuePropName: 'checked' },
58
58
  { type: 'switch', name: 'xoff', label: 'xoff', valuePropName: 'checked' },
59
59
  { type: 'switch', name: 'xany', label: 'xany', valuePropName: 'checked' },
60
- { type: 'select', name: 'lineEnding', label: 'lineEnding', options: commonLineEndings.map(d => ({ value: d.value, label: d.label })) },
60
+ { type: 'select', name: 'txLineEnding', label: 'txLineEnding', options: commonTxLineEndings.map(d => ({ value: d.value, label: d.label })) },
61
+ { type: 'select', name: 'rxLineEnding', label: 'rxLineEnding', options: commonRxLineEndings.map(d => ({ value: d.value, label: d.label })) },
61
62
  commonFields.runScripts,
62
63
  commonFields.description,
63
64
  { type: 'input', name: 'type', label: 'type', hidden: true }
@@ -105,17 +105,11 @@ export default auto(function CmdHistory (props) {
105
105
  ))
106
106
  }
107
107
 
108
- const content = (
109
- <div className='cmd-history-popover-content pd2'>
110
- <div className='cmd-history-search pd2b'>
111
- <InputAutoFocus
112
- value={keyword}
113
- onChange={handleChange}
114
- placeholder={e('search')}
115
- className='cmd-history-search-input'
116
- allowClear
117
- />
118
- </div>
108
+ function renderHeader () {
109
+ if (!historyArray.length) {
110
+ return null
111
+ }
112
+ return (
119
113
  <div className='cmd-history-header pd2b'>
120
114
  <Switch
121
115
  checkedChildren={e('sortByFrequency')}
@@ -130,6 +124,21 @@ export default auto(function CmdHistory (props) {
130
124
  onClick={handleClearAll}
131
125
  />
132
126
  </div>
127
+ )
128
+ }
129
+
130
+ const content = (
131
+ <div className='cmd-history-popover-content pd2'>
132
+ <div className='cmd-history-search pd2b'>
133
+ <InputAutoFocus
134
+ value={keyword}
135
+ onChange={handleChange}
136
+ placeholder={e('search')}
137
+ className='cmd-history-search-input'
138
+ allowClear
139
+ />
140
+ </div>
141
+ {renderHeader()}
133
142
  <div className='cmd-history-list'>
134
143
  {renderList()}
135
144
  </div>
@@ -82,6 +82,7 @@ export function handleTerminalSelectionReplace (event, ctx) {
82
82
  }
83
83
 
84
84
  ctx.term.clearSelection()
85
+ ctx.term.scrollToBottom()
85
86
  return true
86
87
  }
87
88
 
@@ -167,6 +168,7 @@ export function shortcutExtend (Cls) {
167
168
  const altDelDelKey = delKey === 8 ? 127 : 8
168
169
  const char = String.fromCharCode(shiftKey ? delKey : altDelDelKey)
169
170
  this.socket.send(char)
171
+ this.term.scrollToBottom()
170
172
  return false
171
173
  } else if (
172
174
  this.term &&
@@ -193,6 +195,11 @@ export function shortcutExtend (Cls) {
193
195
  this.trzszClient.cancel()
194
196
  return false
195
197
  }
198
+ // Cancel xmodem transfer if active
199
+ if (this.xmodemClient && this.xmodemClient.isActive) {
200
+ this.xmodemClient.cancel()
201
+ return false
202
+ }
196
203
  }
197
204
 
198
205
  let codeName
@@ -38,6 +38,21 @@ export default auto(function HistoryPanel (props) {
38
38
  store.clearHistory()
39
39
  }
40
40
  const e = window.translate
41
+ function renderHeader () {
42
+ if (!arr.length) {
43
+ return null
44
+ }
45
+ return (
46
+ <div className='history-header pd2x pd2b'>
47
+ <Switch
48
+ {...switchProps}
49
+ />
50
+ <UnorderedListOutlined
51
+ {...clearIconProps}
52
+ />
53
+ </div>
54
+ )
55
+ }
41
56
  const switchProps = {
42
57
  checkedChildren: e('sortByFrequency'),
43
58
  unCheckedChildren: e('sortByFrequency'),
@@ -54,14 +69,7 @@ export default auto(function HistoryPanel (props) {
54
69
  <div
55
70
  className='sidebar-panel-history'
56
71
  >
57
- <div className='history-header pd2x pd2b'>
58
- <Switch
59
- {...switchProps}
60
- />
61
- <UnorderedListOutlined
62
- {...clearIconProps}
63
- />
64
- </div>
72
+ {renderHeader()}
65
73
  <div className='history-body'>
66
74
  {
67
75
  arr.map((item, i) => {