@electerm/electerm-react 3.11.12 → 3.15.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/common/is-absolute-path.js +1 -1
- package/client/common/normalize-remote-path.js +20 -0
- package/client/common/resolve.js +16 -0
- 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 +15 -25
- 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/main/main.jsx +1 -1
- package/client/components/sftp/address-bar.jsx +22 -6
- package/client/components/sftp/file-item.jsx +31 -1
- package/client/components/sftp/file-read.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +38 -3
- package/client/components/shortcuts/shortcut-handler.js +7 -0
- package/client/components/sidebar/history.jsx +16 -8
- package/client/components/sys-menu/icons-map.jsx +10 -2
- 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-apis.js +9 -0
- package/client/components/terminal/terminal.jsx +179 -3
- package/client/components/terminal/xmodem-client.js +244 -0
- package/client/components/terminal-info/base.jsx +41 -38
- 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/log-path-edit.jsx +3 -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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure remote path always starts with /
|
|
3
|
+
* Windows drive letters like c: become /c:
|
|
4
|
+
* Also fixes mixed separators like /c:\windows → /c:/windows
|
|
5
|
+
* This is needed because SFTP protocol expects paths with leading /
|
|
6
|
+
* @param {String} path
|
|
7
|
+
* @return {String}
|
|
8
|
+
*/
|
|
9
|
+
export default function normalizeRemotePath (path) {
|
|
10
|
+
if (!path) return path
|
|
11
|
+
// Fix mixed separators: /c:\windows → /c:/windows
|
|
12
|
+
if (/^\/[a-zA-Z]:\\/.test(path)) {
|
|
13
|
+
return path.replace(/\\/g, '/')
|
|
14
|
+
}
|
|
15
|
+
// Add leading / to bare drive letters: c: → /c:, c:\windows → /c:/windows
|
|
16
|
+
if (/^[a-zA-Z]:/.test(path)) {
|
|
17
|
+
return '/' + path.replace(/\\/g, '/')
|
|
18
|
+
}
|
|
19
|
+
return path
|
|
20
|
+
}
|
package/client/common/resolve.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* @return {String}
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export const isWslPath = (path) => /^\\\\(?:wsl\$|wsl\.localhost)\\/.test(path)
|
|
9
|
+
|
|
10
|
+
export const isWslDistroRoot = (path) => {
|
|
11
|
+
const trimmed = path.replace(/\\$/, '')
|
|
12
|
+
return /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(trimmed)
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export default function resolve (basePath, nameOrDot) {
|
|
9
16
|
const hasWinDrive = (path) => /^[a-zA-Z]:/.test(path)
|
|
10
17
|
const isWin = basePath.includes('\\') || nameOrDot.includes('\\') || hasWinDrive(basePath) || hasWinDrive(nameOrDot)
|
|
@@ -15,7 +22,13 @@ export default function resolve (basePath, nameOrDot) {
|
|
|
15
22
|
if (nameOrDot.startsWith('/')) {
|
|
16
23
|
return nameOrDot.replace(/\\/g, sep)
|
|
17
24
|
}
|
|
25
|
+
if (nameOrDot.startsWith('\\\\')) {
|
|
26
|
+
return nameOrDot
|
|
27
|
+
}
|
|
18
28
|
if (nameOrDot === '..') {
|
|
29
|
+
if (isWslDistroRoot(basePath)) {
|
|
30
|
+
return '/'
|
|
31
|
+
}
|
|
19
32
|
const baseEndsWithSep = basePath.endsWith(sep)
|
|
20
33
|
const parts = basePath.split(sep)
|
|
21
34
|
if (parts.length > 1) {
|
|
@@ -27,6 +40,9 @@ export default function resolve (basePath, nameOrDot) {
|
|
|
27
40
|
}
|
|
28
41
|
return '/'
|
|
29
42
|
}
|
|
43
|
+
if (isWslDistroRoot(basePath) && !basePath.endsWith(sep)) {
|
|
44
|
+
return basePath + sep + nameOrDot
|
|
45
|
+
}
|
|
30
46
|
const result = basePath.endsWith(sep) ? basePath + nameOrDot : basePath + sep + nameOrDot
|
|
31
47
|
return isWin && result.length === 3 && result.endsWith(':\\') ? '/' : result
|
|
32
48
|
}
|
|
@@ -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
|
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
Tooltip
|
|
9
9
|
} from 'antd'
|
|
10
10
|
import {
|
|
11
|
-
UserOutlined,
|
|
12
11
|
CopyOutlined,
|
|
13
12
|
CloseOutlined,
|
|
14
13
|
CaretDownOutlined,
|
|
@@ -18,13 +17,11 @@ import { copy } from '../../common/clipboard'
|
|
|
18
17
|
|
|
19
18
|
export default function AIChatHistoryItem ({ item }) {
|
|
20
19
|
const [showOutput, setShowOutput] = useState(true)
|
|
21
|
-
const
|
|
20
|
+
const [isStreaming, setIsStreaming] = useState(false)
|
|
22
21
|
const abortRef = useRef(false)
|
|
23
22
|
const {
|
|
24
23
|
prompt,
|
|
25
|
-
isStreaming,
|
|
26
24
|
sessionId,
|
|
27
|
-
response,
|
|
28
25
|
modelAI,
|
|
29
26
|
roleAI,
|
|
30
27
|
baseURLAI,
|
|
@@ -60,12 +57,11 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
60
57
|
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
61
58
|
if (index !== -1) {
|
|
62
59
|
window.store.aiChatHistory[index].response = streamResponse.content || ''
|
|
63
|
-
window.store.aiChatHistory[index].isStreaming = streamResponse.hasMore
|
|
64
60
|
window.store.aiChatHistory = [...window.store.aiChatHistory]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
}
|
|
62
|
+
setIsStreaming(streamResponse.hasMore)
|
|
63
|
+
if (streamResponse.hasMore) {
|
|
64
|
+
setTimeout(() => pollStreamContent(sid), 200)
|
|
69
65
|
}
|
|
70
66
|
} catch (error) {
|
|
71
67
|
window.store.removeAiHistory(item.id)
|
|
@@ -93,9 +89,9 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
if (aiResponse && aiResponse.isStream && aiResponse.sessionId) {
|
|
92
|
+
setIsStreaming(true)
|
|
96
93
|
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
97
94
|
if (index !== -1) {
|
|
98
|
-
window.store.aiChatHistory[index].isStreaming = true
|
|
99
95
|
window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
|
|
100
96
|
window.store.aiChatHistory[index].response = aiResponse.content || ''
|
|
101
97
|
}
|
|
@@ -104,7 +100,6 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
104
100
|
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
105
101
|
if (index !== -1) {
|
|
106
102
|
window.store.aiChatHistory[index].response = aiResponse.response
|
|
107
|
-
window.store.aiChatHistory[index].isStreaming = false
|
|
108
103
|
}
|
|
109
104
|
}
|
|
110
105
|
} catch (error) {
|
|
@@ -124,12 +119,15 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
124
119
|
proxyAI,
|
|
125
120
|
languageAI
|
|
126
121
|
}
|
|
127
|
-
await runAgentLoop(item, config, abortRef)
|
|
122
|
+
await runAgentLoop(item, config, abortRef, setIsStreaming)
|
|
128
123
|
}, [modelAI, roleAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, languageAI, item.id])
|
|
129
124
|
|
|
130
125
|
useEffect(() => {
|
|
131
|
-
if (
|
|
132
|
-
|
|
126
|
+
if (item.pending) {
|
|
127
|
+
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
128
|
+
if (index !== -1) {
|
|
129
|
+
window.store.aiChatHistory[index].pending = false
|
|
130
|
+
}
|
|
133
131
|
if (mode === 'agent') {
|
|
134
132
|
startAgentRequest()
|
|
135
133
|
} else {
|
|
@@ -142,22 +140,14 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
142
140
|
e.stopPropagation()
|
|
143
141
|
if (mode === 'agent') {
|
|
144
142
|
abortRef.current = true
|
|
145
|
-
|
|
146
|
-
if (index !== -1) {
|
|
147
|
-
window.store.aiChatHistory[index].isStreaming = false
|
|
148
|
-
window.store.aiChatHistory = [...window.store.aiChatHistory]
|
|
149
|
-
}
|
|
143
|
+
setIsStreaming(false)
|
|
150
144
|
return
|
|
151
145
|
}
|
|
152
146
|
if (!sessionId) return
|
|
153
147
|
|
|
154
148
|
try {
|
|
155
149
|
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
|
-
}
|
|
150
|
+
setIsStreaming(false)
|
|
161
151
|
} catch (error) {
|
|
162
152
|
console.error('Error stopping stream:', error)
|
|
163
153
|
}
|
|
@@ -181,7 +171,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
181
171
|
<span className='pointer mg1r' onClick={toggleOutput}>
|
|
182
172
|
{showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
|
183
173
|
</span>
|
|
184
|
-
<
|
|
174
|
+
<span>{prompt}</span>
|
|
185
175
|
{renderStopButton()}
|
|
186
176
|
</div>
|
|
187
177
|
),
|
|
@@ -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 }
|