@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.
Files changed (37) hide show
  1. package/client/common/bookmark-schemas.js +165 -0
  2. package/client/common/constants.js +7 -0
  3. package/client/common/parse-quick-connect.js +13 -10
  4. package/client/common/sanitize-filename.js +66 -0
  5. package/client/common/ws.js +25 -6
  6. package/client/common/zod.js +180 -0
  7. package/client/components/ai/agent-tool-call-card.jsx +90 -0
  8. package/client/components/ai/agent-tools.js +193 -0
  9. package/client/components/ai/agent.js +159 -0
  10. package/client/components/ai/ai-chat-entry.jsx +11 -0
  11. package/client/components/ai/ai-chat-history-item.jsx +48 -2
  12. package/client/components/ai/ai-chat.jsx +25 -6
  13. package/client/components/ai/ai-config.jsx +45 -4
  14. package/client/components/ai/ai.styl +73 -0
  15. package/client/components/bookmark-form/bookmark-schema.js +1 -0
  16. package/client/components/bookmark-form/config/serial.js +2 -1
  17. package/client/components/common/font-select.jsx +45 -0
  18. package/client/components/main/main.jsx +3 -3
  19. package/client/components/rdp/file-transfer.js +3 -0
  20. package/client/components/session/session.jsx +2 -2
  21. package/client/components/setting-panel/setting-terminal.jsx +6 -28
  22. package/client/components/setting-panel/text-bg-modal.jsx +8 -27
  23. package/client/components/setting-sync/setting-sync-form.jsx +1 -1
  24. package/client/components/sftp/file-item.jsx +5 -4
  25. package/client/components/shortcuts/shortcut-handler.js +9 -9
  26. package/client/components/terminal/terminal-error-handle.jsx +1 -1
  27. package/client/components/terminal/terminal-interactive-ui.jsx +157 -0
  28. package/client/components/terminal/terminal-interactive.jsx +64 -163
  29. package/client/components/terminal/terminal.jsx +11 -0
  30. package/client/components/terminal-info/terminal-info-entry.jsx +11 -0
  31. package/client/components/text-editor/text-editor-entry.jsx +11 -0
  32. package/client/components/widgets/widget-form.jsx +27 -2
  33. package/client/entry/worker.js +9 -5
  34. package/client/store/mcp-handler.js +22 -2
  35. package/client/store/watch.js +38 -36
  36. package/package.json +1 -1
  37. 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 = trimmed.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i)
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 = trimmed.slice(protocolMatch[0].length)
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(trimmed)) {
140
+ if (/^[\w.-]+@[\w.-]+/.test(input)) {
138
141
  // user@host or user@host:port
139
142
  protocol = 'ssh'
140
- connectionString = trimmed
141
- } else if (/^[\w.-]+:.*:[\d]+$/.test(trimmed)) {
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 = trimmed
146
- } else if (/^[\w.-]+:[\d]+$/.test(trimmed)) {
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 = trimmed
150
- } else if (/^[\w.-]+$/.test(trimmed)) {
152
+ connectionString = input
153
+ } else if (/^[\w.-]+$/.test(input)) {
151
154
  // just host
152
155
  protocol = 'ssh'
153
- connectionString = trimmed
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
+ }
@@ -57,8 +57,15 @@ class Ws {
57
57
 
58
58
  addEventListener (type = 'message', cb = this.cb) {
59
59
  this.cb = cb
60
- const id = this.eid || generate()
61
- this.eid = id
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
- delete persists[this.eid]
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: this.eid,
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.eid) {
89
- delete persists[this.eid]
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
+ }