@electerm/electerm-react 3.11.11 → 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.
- package/client/common/bookmark-schemas.js +2 -1
- package/client/common/constants.js +11 -2
- package/client/components/ai/agent-tools.js +204 -0
- package/client/components/ai/agent.js +12 -10
- package/client/components/ai/ai-chat-history-item.jsx +14 -23
- package/client/components/ai/ai-chat.jsx +24 -9
- package/client/components/bookmark-form/bookmark-schema.js +2 -1
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/footer/cmd-history.jsx +20 -11
- package/client/components/sftp/file-item.jsx +1 -1
- package/client/components/shortcuts/shortcut-handler.js +7 -0
- package/client/components/sidebar/history.jsx +16 -8
- package/client/components/terminal/attach-addon-custom.js +1 -1
- package/client/components/terminal/drop-file-modal.jsx +53 -22
- package/client/components/terminal/terminal.jsx +68 -1
- package/client/components/terminal/xmodem-client.js +244 -0
- package/client/components/terminal-info/data-cols-parser.jsx +2 -1
- package/client/components/terminal-info/disk.jsx +4 -2
- package/client/components/terminal-info/network.jsx +3 -1
- package/client/components/terminal-info/resource.jsx +3 -3
- package/client/components/tree-list/tree-list.styl +7 -1
- package/client/store/mcp-handler.js +41 -9
- package/client/store/quick-command.js +3 -2
- package/client/store/watch.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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 (
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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,
|
|
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: '
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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>
|
|
@@ -271,7 +271,7 @@ export default class FileSection extends React.Component {
|
|
|
271
271
|
if (!toFile.id || !toFile.isDirectory) {
|
|
272
272
|
toFile = {
|
|
273
273
|
type,
|
|
274
|
-
...getFolderFromFilePath(this.props[type + 'Path']),
|
|
274
|
+
...getFolderFromFilePath(this.props[type + 'Path'], type === typeMap.remote),
|
|
275
275
|
isDirectory: false
|
|
276
276
|
}
|
|
277
277
|
}
|
|
@@ -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
|