@electerm/electerm-react 3.10.0 → 3.11.11
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 +165 -0
- package/client/common/constants.js +7 -0
- package/client/common/parse-quick-connect.js +13 -10
- package/client/common/sanitize-filename.js +66 -0
- package/client/common/ws.js +25 -6
- package/client/common/zod.js +180 -0
- package/client/components/ai/agent-tool-call-card.jsx +90 -0
- package/client/components/ai/agent-tools.js +193 -0
- package/client/components/ai/agent.js +159 -0
- package/client/components/ai/ai-chat-entry.jsx +11 -0
- package/client/components/ai/ai-chat-history-item.jsx +48 -2
- package/client/components/ai/ai-chat.jsx +25 -6
- package/client/components/ai/ai-config.jsx +45 -4
- package/client/components/ai/ai.styl +73 -0
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/config/serial.js +2 -1
- package/client/components/common/font-select.jsx +45 -0
- package/client/components/main/main.jsx +3 -3
- package/client/components/rdp/file-transfer.js +3 -0
- package/client/components/session/session.jsx +2 -2
- package/client/components/setting-panel/setting-terminal.jsx +6 -28
- package/client/components/setting-panel/text-bg-modal.jsx +8 -27
- package/client/components/setting-sync/setting-sync-form.jsx +1 -1
- package/client/components/sftp/file-item.jsx +5 -4
- package/client/components/shortcuts/shortcut-handler.js +9 -9
- package/client/components/terminal/terminal-error-handle.jsx +1 -1
- package/client/components/terminal/terminal-interactive-ui.jsx +157 -0
- package/client/components/terminal/terminal-interactive.jsx +64 -163
- package/client/components/terminal/terminal.jsx +11 -0
- package/client/components/terminal-info/terminal-info-entry.jsx +11 -0
- package/client/components/text-editor/text-editor-entry.jsx +11 -0
- package/client/components/widgets/widget-form.jsx +27 -2
- package/client/entry/worker.js +9 -5
- package/client/store/mcp-handler.js +22 -2
- package/client/store/watch.js +38 -36
- package/package.json +1 -1
- package/client/common/safe-name.js +0 -19
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookmark schemas (ES module version for client)
|
|
3
|
+
* Mirrors src/app/common/bookmark-zod-schemas.js with additional types
|
|
4
|
+
*/
|
|
5
|
+
import { z } from './zod'
|
|
6
|
+
|
|
7
|
+
// const runScriptSchema = z.object({
|
|
8
|
+
// delay: z.number().optional().describe('Delay in ms before executing this command'),
|
|
9
|
+
// script: z.string().describe('Command to execute')
|
|
10
|
+
// })
|
|
11
|
+
|
|
12
|
+
const quickCommandSchema = z.object({
|
|
13
|
+
name: z.string().describe('Quick command name'),
|
|
14
|
+
command: z.string().describe('Command')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const sshTunnelSchema = z.object({
|
|
18
|
+
sshTunnel: z.enum(['forwardRemoteToLocal', 'forwardLocalToRemote', 'dynamicForward']).describe('Tunnel type'),
|
|
19
|
+
sshTunnelLocalHost: z.string().optional().describe('Local host'),
|
|
20
|
+
sshTunnelLocalPort: z.number().optional().describe('Local port'),
|
|
21
|
+
sshTunnelRemoteHost: z.string().optional().describe('Remote host'),
|
|
22
|
+
sshTunnelRemotePort: z.number().optional().describe('Remote port'),
|
|
23
|
+
name: z.string().optional().describe('Tunnel name')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const connectionHoppingSchema = z.object({
|
|
27
|
+
host: z.string().describe('Host address'),
|
|
28
|
+
port: z.number().optional().describe('Port number'),
|
|
29
|
+
username: z.string().optional().describe('Username'),
|
|
30
|
+
password: z.string().optional().describe('Password'),
|
|
31
|
+
privateKey: z.string().optional().describe('Private key'),
|
|
32
|
+
passphrase: z.string().optional().describe('Passphrase'),
|
|
33
|
+
certificate: z.string().optional().describe('Certificate'),
|
|
34
|
+
authType: z.string().optional().describe('Auth type'),
|
|
35
|
+
profile: z.string().optional().describe('Profile id')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const commonNetworkBookmarkProps = {
|
|
39
|
+
title: z.string().describe('Bookmark title'),
|
|
40
|
+
host: z.string().describe('Host address'),
|
|
41
|
+
port: z.number().optional().describe('Port number'),
|
|
42
|
+
username: z.string().optional().describe('Username'),
|
|
43
|
+
password: z.string().optional().describe('Password'),
|
|
44
|
+
description: z.string().optional().describe('Bookmark description'),
|
|
45
|
+
startDirectoryRemote: z.string().optional().describe('Remote starting directory'),
|
|
46
|
+
startDirectoryLocal: z.string().optional().describe('Local starting directory'),
|
|
47
|
+
profile: z.string().optional().describe('Profile id'),
|
|
48
|
+
proxy: z.string().optional().describe('Proxy address (socks5://...)')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const sshBookmarkSchema = {
|
|
52
|
+
...commonNetworkBookmarkProps,
|
|
53
|
+
host: z.string().describe('SSH host address'),
|
|
54
|
+
port: z.number().optional().describe('SSH port (default 22)'),
|
|
55
|
+
username: z.string().optional().describe('SSH username'),
|
|
56
|
+
password: z.string().optional().describe('SSH password'),
|
|
57
|
+
authType: z.enum(['password', 'privateKey', 'profiles']).optional().describe('Authentication type'),
|
|
58
|
+
privateKey: z.string().optional().describe('Private key content or path (for privateKey auth)'),
|
|
59
|
+
passphrase: z.string().optional().describe('Passphrase for private key/certificate'),
|
|
60
|
+
certificate: z.string().optional().describe('Certificate content'),
|
|
61
|
+
enableSsh: z.boolean().optional().describe('Enable ssh, default is true'),
|
|
62
|
+
enableSftp: z.boolean().optional().describe('Enable sftp, default is true'),
|
|
63
|
+
useSshAgent: z.boolean().optional().describe('Use SSH agent, default is true'),
|
|
64
|
+
sshAgent: z.string().optional().describe('SSH agent path'),
|
|
65
|
+
serverHostKey: z.array(z.string()).optional().describe('Server host key algorithms'),
|
|
66
|
+
cipher: z.array(z.string()).optional().describe('Cipher list'),
|
|
67
|
+
quickCommands: z.array(quickCommandSchema).optional().describe('Quick commands'),
|
|
68
|
+
x11: z.boolean().optional().describe('Enable x11 forwarding, default is false'),
|
|
69
|
+
term: z.string().optional().describe('Terminal type, default is xterm-256color'),
|
|
70
|
+
displayRaw: z.boolean().optional().describe('Display raw output, default is false'),
|
|
71
|
+
encode: z.string().optional().describe('Charset, default is utf8'),
|
|
72
|
+
envLang: z.string().optional().describe('ENV LANG, default is en_US.UTF-8'),
|
|
73
|
+
color: z.string().optional().describe('Tag color, like #000000'),
|
|
74
|
+
sshTunnels: z.array(sshTunnelSchema).optional().describe('SSH tunnel definitions'),
|
|
75
|
+
connectionHoppings: z.array(connectionHoppingSchema).optional().describe('Connection hopping definitions')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const telnetBookmarkSchema = {
|
|
79
|
+
...commonNetworkBookmarkProps,
|
|
80
|
+
host: z.string().describe('Telnet host address'),
|
|
81
|
+
port: z.number().optional().describe('Telnet port (default 23)'),
|
|
82
|
+
username: z.string().optional().describe('Telnet username'),
|
|
83
|
+
password: z.string().optional().describe('Telnet password'),
|
|
84
|
+
loginPrompt: z.string().optional().describe('Login prompt regex'),
|
|
85
|
+
passwordPrompt: z.string().optional().describe('Password prompt regex')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const serialBookmarkSchema = {
|
|
89
|
+
title: z.string().describe('Bookmark title'),
|
|
90
|
+
path: z.string().describe('Serial device path, e.g., /dev/ttyUSB0 or COM1'),
|
|
91
|
+
baudRate: z.number().optional().describe('Baud rate (default 9600)'),
|
|
92
|
+
dataBits: z.number().optional().describe('Data bits (default 8)'),
|
|
93
|
+
stopBits: z.number().optional().describe('Stop bits (default 1)'),
|
|
94
|
+
parity: z.enum(['none', 'even', 'odd', 'mark', 'space']).optional().describe('Parity (default none)'),
|
|
95
|
+
rtscts: z.boolean().optional().describe('RTS/CTS flow control'),
|
|
96
|
+
xon: z.boolean().optional().describe('XON flow control'),
|
|
97
|
+
xoff: z.boolean().optional().describe('XOFF flow control'),
|
|
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)'),
|
|
100
|
+
description: z.string().optional().describe('Bookmark description')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const vncBookmarkSchema = {
|
|
104
|
+
...commonNetworkBookmarkProps,
|
|
105
|
+
host: z.string().describe('VNC host address'),
|
|
106
|
+
port: z.number().optional().describe('VNC port (default 5900)'),
|
|
107
|
+
viewOnly: z.boolean().optional().describe('View only mode, default is false'),
|
|
108
|
+
clipViewport: z.boolean().optional().describe('Clip viewport to window'),
|
|
109
|
+
scaleViewport: z.boolean().optional().describe('Scale viewport to window, default is true'),
|
|
110
|
+
qualityLevel: z.number().optional().describe('VNC quality level 0-9, lower is faster, default 3'),
|
|
111
|
+
compressionLevel: z.number().optional().describe('VNC compression level 0-9, lower is faster, default 1'),
|
|
112
|
+
shared: z.boolean().optional().describe('Shared session, default is true')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const rdpBookmarkSchema = {
|
|
116
|
+
...commonNetworkBookmarkProps,
|
|
117
|
+
host: z.string().describe('RDP host address'),
|
|
118
|
+
port: z.number().optional().describe('RDP port (default 3389)'),
|
|
119
|
+
domain: z.string().optional().describe('Login domain')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const ftpBookmarkSchema = {
|
|
123
|
+
title: z.string().describe('Bookmark title'),
|
|
124
|
+
host: z.string().describe('FTP host address'),
|
|
125
|
+
port: z.number().optional().describe('FTP port (default 21)'),
|
|
126
|
+
user: z.string().optional().describe('FTP username'),
|
|
127
|
+
password: z.string().optional().describe('FTP password'),
|
|
128
|
+
secure: z.boolean().optional().describe('Use secure FTP (FTPS), default is false'),
|
|
129
|
+
encode: z.string().optional().describe('Charset for file names, default is utf-8'),
|
|
130
|
+
profile: z.string().optional().describe('Profile id'),
|
|
131
|
+
description: z.string().optional().describe('Bookmark description')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const webBookmarkSchema = {
|
|
135
|
+
url: z.string().describe('Website URL'),
|
|
136
|
+
title: z.string().optional().describe('Bookmark title'),
|
|
137
|
+
description: z.string().optional().describe('Bookmark description'),
|
|
138
|
+
useragent: z.string().optional().describe('Custom user agent')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const localBookmarkSchema = {
|
|
142
|
+
title: z.string().describe('Bookmark title'),
|
|
143
|
+
description: z.string().optional().describe('Bookmark description'),
|
|
144
|
+
startDirectoryLocal: z.string().optional().describe('Local starting directory')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const spiceBookmarkSchema = {
|
|
148
|
+
...commonNetworkBookmarkProps,
|
|
149
|
+
host: z.string().describe('Spice host address'),
|
|
150
|
+
port: z.number().optional().describe('Spice port (default 5900)'),
|
|
151
|
+
viewOnly: z.boolean().optional().describe('View only mode'),
|
|
152
|
+
scaleViewport: z.boolean().optional().describe('Scale viewport to window, default is true')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const bookmarkSchemas = {
|
|
156
|
+
ssh: sshBookmarkSchema,
|
|
157
|
+
telnet: telnetBookmarkSchema,
|
|
158
|
+
serial: serialBookmarkSchema,
|
|
159
|
+
vnc: vncBookmarkSchema,
|
|
160
|
+
rdp: rdpBookmarkSchema,
|
|
161
|
+
ftp: ftpBookmarkSchema,
|
|
162
|
+
web: webBookmarkSchema,
|
|
163
|
+
local: localBookmarkSchema,
|
|
164
|
+
spice: spiceBookmarkSchema
|
|
165
|
+
}
|
|
@@ -180,6 +180,13 @@ export const commonParities = [
|
|
|
180
180
|
'none', 'even', 'mark', 'odd', 'space'
|
|
181
181
|
]
|
|
182
182
|
|
|
183
|
+
export const commonLineEndings = [
|
|
184
|
+
{ value: '', label: 'none' },
|
|
185
|
+
{ value: '\r', label: 'CR' },
|
|
186
|
+
{ value: '\n', label: 'LF' },
|
|
187
|
+
{ value: '\r\n', label: 'CR+LF' }
|
|
188
|
+
]
|
|
189
|
+
|
|
183
190
|
export const maxBatchInput = 30
|
|
184
191
|
export const windowControlWidth = 94
|
|
185
192
|
export const baseUpdateCheckUrls = [
|
|
@@ -115,8 +115,11 @@ function parseQuickConnect (str) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
try {
|
|
118
|
+
// Strip trailing slashes (supports pasted URLs like host/ or ssh://host/)
|
|
119
|
+
const input = trimmed.replace(/\/+$/, '')
|
|
120
|
+
|
|
118
121
|
// Detect protocol
|
|
119
|
-
const protocolMatch =
|
|
122
|
+
const protocolMatch = input.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i)
|
|
120
123
|
|
|
121
124
|
let protocol = ''
|
|
122
125
|
let connectionString = ''
|
|
@@ -129,28 +132,28 @@ function parseQuickConnect (str) {
|
|
|
129
132
|
if (protocol === 'http' || protocol === 'https') {
|
|
130
133
|
protocol = 'web'
|
|
131
134
|
}
|
|
132
|
-
connectionString =
|
|
135
|
+
connectionString = input.slice(protocolMatch[0].length)
|
|
133
136
|
} else {
|
|
134
137
|
// Shortcut format - default to SSH
|
|
135
138
|
// Match user@host or user@host:port or just host or host:port
|
|
136
139
|
// Use last colon to determine port for host:port format
|
|
137
|
-
if (/^[\w.-]+@[\w.-]+/.test(
|
|
140
|
+
if (/^[\w.-]+@[\w.-]+/.test(input)) {
|
|
138
141
|
// user@host or user@host:port
|
|
139
142
|
protocol = 'ssh'
|
|
140
|
-
connectionString =
|
|
141
|
-
} else if (/^[\w.-]+:.*:[\d]+$/.test(
|
|
143
|
+
connectionString = input
|
|
144
|
+
} else if (/^[\w.-]+:.*:[\d]+$/.test(input)) {
|
|
142
145
|
// host:port format with colons in hostname (e.g., localhost:23344, zxd:localhost:23344)
|
|
143
146
|
// Check if the last colon is followed by digits (port number)
|
|
144
147
|
protocol = 'ssh'
|
|
145
|
-
connectionString =
|
|
146
|
-
} else if (/^[\w.-]+:[\d]+$/.test(
|
|
148
|
+
connectionString = input
|
|
149
|
+
} else if (/^[\w.-]+:[\d]+$/.test(input)) {
|
|
147
150
|
// host:port (no username, simple format like host:22)
|
|
148
151
|
protocol = 'ssh'
|
|
149
|
-
connectionString =
|
|
150
|
-
} else if (/^[\w.-]+$/.test(
|
|
152
|
+
connectionString = input
|
|
153
|
+
} else if (/^[\w.-]+$/.test(input)) {
|
|
151
154
|
// just host
|
|
152
155
|
protocol = 'ssh'
|
|
153
|
-
connectionString =
|
|
156
|
+
connectionString = input
|
|
154
157
|
} else {
|
|
155
158
|
return null
|
|
156
159
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a filename for cross-platform file transfers.
|
|
3
|
+
*
|
|
4
|
+
* When transferring files between different OS (Linux <-> Windows <-> macOS),
|
|
5
|
+
* filenames may contain characters that are illegal on the destination OS.
|
|
6
|
+
* Windows is the most restrictive common platform, so we use its rules as
|
|
7
|
+
* the baseline for maximum compatibility.
|
|
8
|
+
*
|
|
9
|
+
* Rules applied:
|
|
10
|
+
* - Remove control characters (0x00-0x1F)
|
|
11
|
+
* - Replace reserved characters: < > : " / \ | ? * with _
|
|
12
|
+
* - Remove leading/trailing dots and spaces (Windows restriction)
|
|
13
|
+
* - Reject reserved Windows device names: CON, PRN, AUX, NUL, COM1-9, LPT1-9
|
|
14
|
+
* - Limit filename length to 255 bytes (common filesystem limit)
|
|
15
|
+
* - Fallback to 'unnamed' if result is empty
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Characters illegal on Windows (and problematic on many systems)
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
const ILLEGAL_CHARS = /[<>:"/\\|?\x00-\x1f]/g
|
|
21
|
+
|
|
22
|
+
// Leading/trailing dots and spaces are problematic on Windows
|
|
23
|
+
const LEADING_TRAILING = /^[.\s]+|[.\s]+$/g
|
|
24
|
+
|
|
25
|
+
// Reserved Windows device names (case-insensitive)
|
|
26
|
+
const RESERVED_NAMES = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i
|
|
27
|
+
|
|
28
|
+
const MAX_FILENAME_LENGTH = 255
|
|
29
|
+
|
|
30
|
+
const REPLACEMENT_CHAR = '_'
|
|
31
|
+
|
|
32
|
+
export default function sanitizeFilename (name) {
|
|
33
|
+
if (!name || typeof name !== 'string') {
|
|
34
|
+
return 'unnamed'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let safe = name
|
|
38
|
+
// Replace illegal characters
|
|
39
|
+
.replace(ILLEGAL_CHARS, REPLACEMENT_CHAR)
|
|
40
|
+
// Strip leading/trailing dots and spaces
|
|
41
|
+
.replace(LEADING_TRAILING, '')
|
|
42
|
+
|
|
43
|
+
// Handle reserved Windows device names by appending underscore
|
|
44
|
+
if (RESERVED_NAMES.test(safe)) {
|
|
45
|
+
safe = safe + REPLACEMENT_CHAR
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Truncate to max length
|
|
49
|
+
if (safe.length > MAX_FILENAME_LENGTH) {
|
|
50
|
+
const ext = safe.lastIndexOf('.')
|
|
51
|
+
if (ext > 0) {
|
|
52
|
+
// Preserve extension when truncating
|
|
53
|
+
const extension = safe.slice(ext)
|
|
54
|
+
safe = safe.slice(0, MAX_FILENAME_LENGTH - extension.length) + extension
|
|
55
|
+
} else {
|
|
56
|
+
safe = safe.slice(0, MAX_FILENAME_LENGTH)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback for empty result
|
|
61
|
+
if (!safe) {
|
|
62
|
+
return 'unnamed'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return safe
|
|
66
|
+
}
|
package/client/common/ws.js
CHANGED
|
@@ -57,8 +57,15 @@ class Ws {
|
|
|
57
57
|
|
|
58
58
|
addEventListener (type = 'message', cb = this.cb) {
|
|
59
59
|
this.cb = cb
|
|
60
|
-
const id =
|
|
61
|
-
this.
|
|
60
|
+
const id = generate()
|
|
61
|
+
if (!this.eids) {
|
|
62
|
+
this.eids = new Set()
|
|
63
|
+
}
|
|
64
|
+
if (!this.cbToId) {
|
|
65
|
+
this.cbToId = new Map()
|
|
66
|
+
}
|
|
67
|
+
this.eids.add(id)
|
|
68
|
+
this.cbToId.set(cb, id)
|
|
62
69
|
persists[id] = {
|
|
63
70
|
resolve: cb
|
|
64
71
|
}
|
|
@@ -71,9 +78,15 @@ class Ws {
|
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
removeEventListener (type, cb) {
|
|
74
|
-
|
|
81
|
+
if (!this.cbToId || !this.cbToId.has(cb)) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const id = this.cbToId.get(cb)
|
|
85
|
+
this.cbToId.delete(cb)
|
|
86
|
+
this.eids.delete(id)
|
|
87
|
+
delete persists[id]
|
|
75
88
|
send({
|
|
76
|
-
id
|
|
89
|
+
id,
|
|
77
90
|
wsId: this.id,
|
|
78
91
|
type,
|
|
79
92
|
action: 'removeEventListener'
|
|
@@ -85,8 +98,14 @@ class Ws {
|
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
clearOnces () {
|
|
88
|
-
if (this.
|
|
89
|
-
|
|
101
|
+
if (this.eids) {
|
|
102
|
+
for (const eid of this.eids) {
|
|
103
|
+
delete persists[eid]
|
|
104
|
+
}
|
|
105
|
+
this.eids.clear()
|
|
106
|
+
}
|
|
107
|
+
if (this.cbToId) {
|
|
108
|
+
this.cbToId.clear()
|
|
90
109
|
}
|
|
91
110
|
const ids = [...this.onceIds]
|
|
92
111
|
ids.forEach(k => {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight zod replacement (ES module version for client)
|
|
3
|
+
* Covers only the API surface used in the project:
|
|
4
|
+
* z.string(), z.number(), z.boolean(), z.any(),
|
|
5
|
+
* z.enum(), z.object(), z.array(), z.record(),
|
|
6
|
+
* .optional(), .describe(), z.toJSONSchema()
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class ZodType {
|
|
10
|
+
constructor (typeName, meta = {}) {
|
|
11
|
+
this._typeName = typeName
|
|
12
|
+
this._optional = false
|
|
13
|
+
this._description = undefined
|
|
14
|
+
this._meta = meta
|
|
15
|
+
this['~standard'] = { type: typeName }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
optional () {
|
|
19
|
+
const clone = this._clone()
|
|
20
|
+
clone._optional = true
|
|
21
|
+
return clone
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe (desc) {
|
|
25
|
+
const clone = this._clone()
|
|
26
|
+
clone._description = desc
|
|
27
|
+
return clone
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_clone () {
|
|
31
|
+
const clone = Object.create(Object.getPrototypeOf(this))
|
|
32
|
+
Object.assign(clone, this)
|
|
33
|
+
clone['~standard'] = { ...this['~standard'] }
|
|
34
|
+
return clone
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_toJsonSchema () {
|
|
38
|
+
throw new Error('_toJsonSchema not implemented for ' + this._typeName)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ZodString extends ZodType {
|
|
43
|
+
constructor () {
|
|
44
|
+
super('string')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_toJsonSchema () {
|
|
48
|
+
return { type: 'string' }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ZodNumber extends ZodType {
|
|
53
|
+
constructor () {
|
|
54
|
+
super('number')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_toJsonSchema () {
|
|
58
|
+
return { type: 'number' }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class ZodBoolean extends ZodType {
|
|
63
|
+
constructor () {
|
|
64
|
+
super('boolean')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_toJsonSchema () {
|
|
68
|
+
return { type: 'boolean' }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class ZodEnum extends ZodType {
|
|
73
|
+
constructor (values) {
|
|
74
|
+
super('enum', { values })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_toJsonSchema () {
|
|
78
|
+
return { type: 'string', enum: this._meta.values }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class ZodArray extends ZodType {
|
|
83
|
+
constructor (itemSchema) {
|
|
84
|
+
super('array', { itemSchema })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_toJsonSchema () {
|
|
88
|
+
const items = schemaToJsonSchema(this._meta.itemSchema)
|
|
89
|
+
return { type: 'array', items }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class ZodObject extends ZodType {
|
|
94
|
+
constructor (shape) {
|
|
95
|
+
super('object', { shape })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_toJsonSchema () {
|
|
99
|
+
const properties = {}
|
|
100
|
+
const required = []
|
|
101
|
+
const shape = this._meta.shape || {}
|
|
102
|
+
for (const [key, schema] of Object.entries(shape)) {
|
|
103
|
+
properties[key] = schemaToJsonSchema(schema)
|
|
104
|
+
if (schema._description) {
|
|
105
|
+
properties[key].description = schema._description
|
|
106
|
+
}
|
|
107
|
+
if (!schema._optional) {
|
|
108
|
+
required.push(key)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const result = { type: 'object', properties }
|
|
112
|
+
if (required.length > 0) {
|
|
113
|
+
result.required = required
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function schemaToJsonSchema (schema) {
|
|
120
|
+
if (!schema) {
|
|
121
|
+
return {}
|
|
122
|
+
}
|
|
123
|
+
if (schema instanceof ZodType) {
|
|
124
|
+
const base = schema._toJsonSchema()
|
|
125
|
+
if (schema._description) {
|
|
126
|
+
base.description = schema._description
|
|
127
|
+
}
|
|
128
|
+
return base
|
|
129
|
+
}
|
|
130
|
+
if (typeof schema === 'object' && !Array.isArray(schema)) {
|
|
131
|
+
return objectShapeToJsonSchema(schema)
|
|
132
|
+
}
|
|
133
|
+
return {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function objectShapeToJsonSchema (shape) {
|
|
137
|
+
const properties = {}
|
|
138
|
+
const required = []
|
|
139
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
140
|
+
if (value instanceof ZodType) {
|
|
141
|
+
properties[key] = schemaToJsonSchema(value)
|
|
142
|
+
if (value._description) {
|
|
143
|
+
properties[key].description = value._description
|
|
144
|
+
}
|
|
145
|
+
if (!value._optional) {
|
|
146
|
+
required.push(key)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const result = { type: 'object', properties }
|
|
151
|
+
if (required.length > 0) {
|
|
152
|
+
result.required = required
|
|
153
|
+
}
|
|
154
|
+
return result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const z = {
|
|
158
|
+
string: () => new ZodString(),
|
|
159
|
+
number: () => new ZodNumber(),
|
|
160
|
+
boolean: () => new ZodBoolean(),
|
|
161
|
+
enum: (values) => new ZodEnum(values),
|
|
162
|
+
object: (shape) => new ZodObject(shape || {}),
|
|
163
|
+
array: (itemSchema) => new ZodArray(itemSchema),
|
|
164
|
+
toJSONSchema: (schema) => {
|
|
165
|
+
if (schema instanceof ZodType) {
|
|
166
|
+
return schema._toJsonSchema()
|
|
167
|
+
}
|
|
168
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
169
|
+
const hasZodValues = Object.values(schema).some(
|
|
170
|
+
v => v instanceof ZodType
|
|
171
|
+
)
|
|
172
|
+
if (hasZodValues) {
|
|
173
|
+
return objectShapeToJsonSchema(schema)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { type: 'object', properties: {} }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { ZodType, schemaToJsonSchema }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Tag } from 'antd'
|
|
3
|
+
import {
|
|
4
|
+
CaretDownOutlined,
|
|
5
|
+
CaretRightOutlined,
|
|
6
|
+
LoadingOutlined,
|
|
7
|
+
CheckCircleOutlined,
|
|
8
|
+
CloseCircleOutlined,
|
|
9
|
+
CodeOutlined,
|
|
10
|
+
DatabaseOutlined
|
|
11
|
+
} from '@ant-design/icons'
|
|
12
|
+
|
|
13
|
+
const toolIcons = {
|
|
14
|
+
send_terminal_command: CodeOutlined,
|
|
15
|
+
get_terminal_output: CodeOutlined,
|
|
16
|
+
open_local_terminal: CodeOutlined,
|
|
17
|
+
list_tabs: CodeOutlined,
|
|
18
|
+
get_active_tab: CodeOutlined,
|
|
19
|
+
switch_tab: CodeOutlined,
|
|
20
|
+
list_bookmarks: DatabaseOutlined,
|
|
21
|
+
open_bookmark: DatabaseOutlined,
|
|
22
|
+
add_bookmark: DatabaseOutlined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatResult (result) {
|
|
26
|
+
if (!result) return ''
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(result)
|
|
29
|
+
if (parsed.output) return parsed.output
|
|
30
|
+
return JSON.stringify(parsed, null, 2)
|
|
31
|
+
} catch {
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function AgentToolCallCard ({ toolCall }) {
|
|
37
|
+
const [expanded, setExpanded] = useState(toolCall.status === 'running')
|
|
38
|
+
const { name, args, status, result } = toolCall
|
|
39
|
+
const Icon = toolIcons[name] || CodeOutlined
|
|
40
|
+
|
|
41
|
+
function renderStatus () {
|
|
42
|
+
if (status === 'running') {
|
|
43
|
+
return <LoadingOutlined className='agent-tool-status-running' />
|
|
44
|
+
}
|
|
45
|
+
if (status === 'completed') {
|
|
46
|
+
return <CheckCircleOutlined className='agent-tool-status-completed' />
|
|
47
|
+
}
|
|
48
|
+
return <CloseCircleOutlined className='agent-tool-status-error' />
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderTag () {
|
|
52
|
+
const color = status === 'running' ? 'processing' : status === 'completed' ? 'success' : 'error'
|
|
53
|
+
return (
|
|
54
|
+
<Tag color={color} className='agent-tool-tag'>
|
|
55
|
+
{status}
|
|
56
|
+
</Tag>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={`agent-tool-call-card agent-tool-${status}`}>
|
|
62
|
+
<div
|
|
63
|
+
className='agent-tool-header pointer'
|
|
64
|
+
onClick={() => setExpanded(!expanded)}
|
|
65
|
+
>
|
|
66
|
+
{expanded ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
|
67
|
+
<Icon className='mg1l' />
|
|
68
|
+
<span className='mg1l agent-tool-name'>{name}</span>
|
|
69
|
+
{renderTag()}
|
|
70
|
+
{renderStatus()}
|
|
71
|
+
</div>
|
|
72
|
+
{expanded && (
|
|
73
|
+
<div className='agent-tool-detail'>
|
|
74
|
+
{args && Object.keys(args).length > 0 && (
|
|
75
|
+
<div className='agent-tool-args'>
|
|
76
|
+
<div className='agent-tool-label'>Arguments:</div>
|
|
77
|
+
<pre className='agent-tool-pre'>{JSON.stringify(args, null, 2)}</pre>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
{result && (
|
|
81
|
+
<div className='agent-tool-result'>
|
|
82
|
+
<div className='agent-tool-label'>Result:</div>
|
|
83
|
+
<pre className='agent-tool-pre'>{formatResult(result)}</pre>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|