@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,193 @@
1
+ import { z } from '../../common/zod'
2
+ import { bookmarkSchemas } from '../../common/bookmark-schemas'
3
+
4
+ function buildAddBookmarkParameters () {
5
+ const typeProperties = {}
6
+ for (const [type, schema] of Object.entries(bookmarkSchemas)) {
7
+ typeProperties[type] = z.toJSONSchema(z.object(schema))
8
+ }
9
+
10
+ return {
11
+ type: 'object',
12
+ properties: {
13
+ type: {
14
+ type: 'string',
15
+ enum: Object.keys(bookmarkSchemas),
16
+ description: 'Bookmark type'
17
+ },
18
+ ...Object.fromEntries(
19
+ Object.entries(typeProperties).map(([type, schema]) => [
20
+ type,
21
+ { type: 'object', description: `Fields for ${type} bookmark`, ...schema }
22
+ ])
23
+ )
24
+ },
25
+ required: ['type']
26
+ }
27
+ }
28
+
29
+ export const agentTools = [
30
+ {
31
+ type: 'function',
32
+ function: {
33
+ name: 'send_terminal_command',
34
+ description: 'Send a command to a terminal tab and wait for it to finish. Returns the command output.',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ command: {
39
+ type: 'string',
40
+ description: 'The shell command to execute'
41
+ },
42
+ tabId: {
43
+ type: 'string',
44
+ description: 'Terminal tab ID. Omit to use the active terminal.'
45
+ }
46
+ },
47
+ required: ['command']
48
+ }
49
+ }
50
+ },
51
+ {
52
+ type: 'function',
53
+ function: {
54
+ name: 'get_terminal_output',
55
+ description: 'Read the current visible output from a terminal.',
56
+ parameters: {
57
+ type: 'object',
58
+ properties: {
59
+ tabId: {
60
+ type: 'string',
61
+ description: 'Terminal tab ID. Omit for active terminal.'
62
+ },
63
+ lines: {
64
+ type: 'number',
65
+ description: 'Number of recent lines to read (default 50).'
66
+ }
67
+ }
68
+ }
69
+ }
70
+ },
71
+ {
72
+ type: 'function',
73
+ function: {
74
+ name: 'open_local_terminal',
75
+ description: 'Open a new local terminal tab. Returns the new tab ID.',
76
+ parameters: {
77
+ type: 'object',
78
+ properties: {}
79
+ }
80
+ }
81
+ },
82
+ {
83
+ type: 'function',
84
+ function: {
85
+ name: 'list_tabs',
86
+ description: 'List all open terminal tabs with their IDs, titles, hosts, and types.',
87
+ parameters: {
88
+ type: 'object',
89
+ properties: {}
90
+ }
91
+ }
92
+ },
93
+ {
94
+ type: 'function',
95
+ function: {
96
+ name: 'get_active_tab',
97
+ description: 'Get the currently active terminal tab.',
98
+ parameters: {
99
+ type: 'object',
100
+ properties: {}
101
+ }
102
+ }
103
+ },
104
+ {
105
+ type: 'function',
106
+ function: {
107
+ name: 'switch_tab',
108
+ description: 'Switch to a different terminal tab.',
109
+ parameters: {
110
+ type: 'object',
111
+ properties: {
112
+ tabId: {
113
+ type: 'string',
114
+ description: 'The tab ID to switch to.'
115
+ }
116
+ },
117
+ required: ['tabId']
118
+ }
119
+ }
120
+ },
121
+ {
122
+ type: 'function',
123
+ function: {
124
+ name: 'list_bookmarks',
125
+ description: 'List all saved bookmarks (SSH, Telnet, VNC, etc.).',
126
+ parameters: {
127
+ type: 'object',
128
+ properties: {}
129
+ }
130
+ }
131
+ },
132
+ {
133
+ type: 'function',
134
+ function: {
135
+ name: 'open_bookmark',
136
+ description: 'Open a saved bookmark as a new terminal tab.',
137
+ parameters: {
138
+ type: 'object',
139
+ properties: {
140
+ id: {
141
+ type: 'string',
142
+ description: 'The bookmark ID to open.'
143
+ }
144
+ },
145
+ required: ['id']
146
+ }
147
+ }
148
+ },
149
+ {
150
+ type: 'function',
151
+ function: {
152
+ name: 'add_bookmark',
153
+ description: 'Create a new bookmark. Specify the type and provide type-specific fields. Supported types: ' + Object.keys(bookmarkSchemas).join(', ') + '.',
154
+ parameters: buildAddBookmarkParameters()
155
+ }
156
+ }
157
+ ]
158
+
159
+ export async function executeToolCall (toolName, args) {
160
+ const store = window.store
161
+ switch (toolName) {
162
+ case 'send_terminal_command': {
163
+ store.mcpSendTerminalCommand(args)
164
+ const idleResult = await store.mcpWaitForTerminalIdle({
165
+ tabId: args.tabId || store.activeTabId,
166
+ timeout: 30000,
167
+ lines: 100
168
+ })
169
+ return JSON.stringify(idleResult)
170
+ }
171
+ case 'get_terminal_output':
172
+ return JSON.stringify(store.mcpGetTerminalOutput(args))
173
+ case 'open_local_terminal':
174
+ return JSON.stringify(store.mcpOpenLocalTerminal())
175
+ case 'list_tabs':
176
+ return JSON.stringify(store.mcpListTabs())
177
+ case 'get_active_tab':
178
+ return JSON.stringify(store.mcpGetActiveTab())
179
+ case 'switch_tab':
180
+ return JSON.stringify(store.mcpSwitchTab(args))
181
+ case 'list_bookmarks':
182
+ return JSON.stringify(store.mcpListBookmarks())
183
+ case 'open_bookmark':
184
+ return JSON.stringify(store.mcpOpenBookmark(args))
185
+ case 'add_bookmark': {
186
+ const { type } = args
187
+ const typeFields = args[type] || {}
188
+ return JSON.stringify(await store.mcpAddBookmark({ type, ...typeFields }))
189
+ }
190
+ default:
191
+ throw new Error(`Unknown agent tool: ${toolName}`)
192
+ }
193
+ }
@@ -0,0 +1,159 @@
1
+ import { agentTools, executeToolCall } from './agent-tools'
2
+
3
+ const MAX_ITERATIONS = 15
4
+
5
+ function buildAgentSystemPrompt (config) {
6
+ const lang = config.languageAI || window.store.getLangName()
7
+ const baseRole = config.roleAI || 'You are a helpful assistant.'
8
+ return `${baseRole}
9
+
10
+ You are operating inside electerm, a terminal/SSH client. You have access to tools that let you:
11
+ - Run commands in terminal tabs and read their output
12
+ - Open new terminal tabs (local or SSH)
13
+ - Manage bookmarks (create, list, open connections)
14
+ - Switch between tabs
15
+
16
+ When the user asks you to perform terminal operations, use the available tools.
17
+ Always explain what you are doing before executing commands.
18
+ If a command produces errors, analyze the output and try to fix the issue.
19
+ Prefer using the active terminal unless the user specifies otherwise.
20
+ For SSH connections, create a bookmark and open it rather than running ssh directly.
21
+
22
+ Reply in ${lang} language.`
23
+ }
24
+
25
+ function updateChatEntry (chatEntry, updates) {
26
+ const index = window.store.aiChatHistory.findIndex(i => i.id === chatEntry.id)
27
+ if (index !== -1) {
28
+ Object.assign(window.store.aiChatHistory[index], updates)
29
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
30
+ }
31
+ }
32
+
33
+ async function callBackendAIchatWithTools (messages, config) {
34
+ return window.pre.runGlobalAsync(
35
+ 'AIchatWithTools',
36
+ messages,
37
+ config.modelAI,
38
+ config.baseURLAI,
39
+ config.apiPathAI,
40
+ config.apiKeyAI,
41
+ config.proxyAI,
42
+ agentTools
43
+ )
44
+ }
45
+
46
+ export async function runAgentLoop (chatEntry, config, abortRef) {
47
+ const messages = [
48
+ { role: 'system', content: buildAgentSystemPrompt(config) },
49
+ { role: 'user', content: chatEntry.prompt }
50
+ ]
51
+ const toolCallsLog = []
52
+ let accumulatedContent = ''
53
+
54
+ updateChatEntry(chatEntry, {
55
+ isStreaming: true,
56
+ toolCalls: [],
57
+ response: ''
58
+ })
59
+
60
+ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
61
+ if (abortRef && abortRef.current) {
62
+ updateChatEntry(chatEntry, {
63
+ isStreaming: false,
64
+ response: accumulatedContent + '\n\n*(Agent stopped by user)*'
65
+ })
66
+ return
67
+ }
68
+
69
+ const result = await callBackendAIchatWithTools(messages, config)
70
+
71
+ if (result.error) {
72
+ updateChatEntry(chatEntry, {
73
+ isStreaming: false,
74
+ response: accumulatedContent + `\n\n**Error:** ${result.error}`
75
+ })
76
+ return
77
+ }
78
+
79
+ const assistantMessage = result.message
80
+ if (!assistantMessage) {
81
+ updateChatEntry(chatEntry, {
82
+ isStreaming: false,
83
+ response: accumulatedContent || 'No response from AI.'
84
+ })
85
+ return
86
+ }
87
+
88
+ messages.push(assistantMessage)
89
+
90
+ if (assistantMessage.content) {
91
+ accumulatedContent += (accumulatedContent ? '\n\n' : '') + assistantMessage.content
92
+ updateChatEntry(chatEntry, {
93
+ response: accumulatedContent
94
+ })
95
+ }
96
+
97
+ if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
98
+ updateChatEntry(chatEntry, {
99
+ isStreaming: false,
100
+ response: accumulatedContent
101
+ })
102
+ return
103
+ }
104
+
105
+ for (const toolCall of assistantMessage.tool_calls) {
106
+ if (abortRef && abortRef.current) {
107
+ updateChatEntry(chatEntry, {
108
+ isStreaming: false,
109
+ response: accumulatedContent + '\n\n*(Agent stopped by user)*'
110
+ })
111
+ return
112
+ }
113
+
114
+ let args
115
+ try {
116
+ args = JSON.parse(toolCall.function.arguments)
117
+ } catch {
118
+ args = {}
119
+ }
120
+
121
+ const toolEntry = {
122
+ id: toolCall.id,
123
+ name: toolCall.function.name,
124
+ args,
125
+ status: 'running',
126
+ result: null
127
+ }
128
+ toolCallsLog.push(toolEntry)
129
+ updateChatEntry(chatEntry, {
130
+ toolCalls: [...toolCallsLog]
131
+ })
132
+
133
+ let toolResult
134
+ try {
135
+ toolResult = await executeToolCall(toolCall.function.name, args)
136
+ toolEntry.status = 'completed'
137
+ toolEntry.result = toolResult
138
+ } catch (err) {
139
+ toolEntry.status = 'error'
140
+ toolEntry.result = err.message
141
+ }
142
+
143
+ updateChatEntry(chatEntry, {
144
+ toolCalls: [...toolCallsLog]
145
+ })
146
+
147
+ messages.push({
148
+ role: 'tool',
149
+ tool_call_id: toolCall.id,
150
+ content: toolEntry.result
151
+ })
152
+ }
153
+ }
154
+
155
+ updateChatEntry(chatEntry, {
156
+ isStreaming: false,
157
+ response: accumulatedContent + '\n\n*(Agent reached maximum iterations)*'
158
+ })
159
+ }
@@ -0,0 +1,11 @@
1
+ import { lazy, Suspense } from 'react'
2
+
3
+ const AIChat = lazy(() => import('./ai-chat'))
4
+
5
+ export default function AIChatEntry (props) {
6
+ return (
7
+ <Suspense fallback={null}>
8
+ <AIChat {...props} />
9
+ </Suspense>
10
+ )
11
+ }
@@ -1,6 +1,8 @@
1
1
  import { useState, useEffect, useRef, useCallback } from 'react'
2
2
  import AIOutput from './ai-output'
3
3
  import AIStopIcon from './ai-stop-icon'
4
+ import AgentToolCallCard from './agent-tool-call-card'
5
+ import { runAgentLoop } from './agent'
4
6
  import {
5
7
  Alert,
6
8
  Tooltip
@@ -17,6 +19,7 @@ import { copy } from '../../common/clipboard'
17
19
  export default function AIChatHistoryItem ({ item }) {
18
20
  const [showOutput, setShowOutput] = useState(true)
19
21
  const startedRef = useRef(false)
22
+ const abortRef = useRef(false)
20
23
  const {
21
24
  prompt,
22
25
  isStreaming,
@@ -28,7 +31,9 @@ export default function AIChatHistoryItem ({ item }) {
28
31
  apiPathAI,
29
32
  apiKeyAI,
30
33
  proxyAI,
31
- languageAI
34
+ languageAI,
35
+ mode,
36
+ toolCalls
32
37
  } = item
33
38
 
34
39
  function toggleOutput () {
@@ -108,15 +113,42 @@ export default function AIChatHistoryItem ({ item }) {
108
113
  }
109
114
  }, [prompt, modelAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, item.id, pollStreamContent])
110
115
 
116
+ const startAgentRequest = useCallback(async () => {
117
+ abortRef.current = false
118
+ const config = {
119
+ modelAI,
120
+ roleAI,
121
+ baseURLAI,
122
+ apiPathAI,
123
+ apiKeyAI,
124
+ proxyAI,
125
+ languageAI
126
+ }
127
+ await runAgentLoop(item, config, abortRef)
128
+ }, [modelAI, roleAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, languageAI, item.id])
129
+
111
130
  useEffect(() => {
112
131
  if (!response && !startedRef.current) {
113
132
  startedRef.current = true
114
- startRequest()
133
+ if (mode === 'agent') {
134
+ startAgentRequest()
135
+ } else {
136
+ startRequest()
137
+ }
115
138
  }
116
139
  }, [])
117
140
 
118
141
  async function handleStop (e) {
119
142
  e.stopPropagation()
143
+ if (mode === 'agent') {
144
+ abortRef.current = true
145
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
146
+ if (index !== -1) {
147
+ window.store.aiChatHistory[index].isStreaming = false
148
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
149
+ }
150
+ return
151
+ }
120
152
  if (!sessionId) return
121
153
 
122
154
  try {
@@ -194,6 +226,19 @@ export default function AIChatHistoryItem ({ item }) {
194
226
  )
195
227
  }
196
228
 
229
+ function renderToolCalls () {
230
+ if (mode !== 'agent' || !toolCalls || !toolCalls.length) {
231
+ return null
232
+ }
233
+ return (
234
+ <div className='agent-tool-calls'>
235
+ {toolCalls.map((tc) => (
236
+ <AgentToolCallCard key={tc.id} toolCall={tc} />
237
+ ))}
238
+ </div>
239
+ )
240
+ }
241
+
197
242
  return (
198
243
  <div className='chat-history-item'>
199
244
  <div className='mg1y'>
@@ -201,6 +246,7 @@ export default function AIChatHistoryItem ({ item }) {
201
246
  <Alert {...alertProps} />
202
247
  </Tooltip>
203
248
  </div>
249
+ {renderToolCalls()}
204
250
  {showOutput && <AIOutput item={item} />}
205
251
  </div>
206
252
  )
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useEffect } from 'react'
2
- import { Flex, Input } from 'antd'
2
+ import { Flex, Input, 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'
@@ -21,6 +21,8 @@ const MAX_HISTORY = 100
21
21
 
22
22
  export default function AIChat (props) {
23
23
  const [prompt, setPrompt] = useState('')
24
+ const [mode, setMode] = useState('ask')
25
+ const isAgent = mode === 'agent'
24
26
 
25
27
  function handlePromptChange (e) {
26
28
  setPrompt(e.target.value)
@@ -38,6 +40,8 @@ export default function AIChat (props) {
38
40
  response: '',
39
41
  isStreaming: false,
40
42
  sessionId: null,
43
+ mode,
44
+ toolCalls: [],
41
45
  ...pick(props.config, [
42
46
  'modelAI',
43
47
  'roleAI',
@@ -57,7 +61,7 @@ export default function AIChat (props) {
57
61
  if (window.store.aiChatHistory.length > MAX_HISTORY) {
58
62
  window.store.aiChatHistory.splice(MAX_HISTORY)
59
63
  }
60
- }, [prompt])
64
+ }, [prompt, mode])
61
65
 
62
66
  function renderHistory () {
63
67
  return (
@@ -75,6 +79,19 @@ export default function AIChat (props) {
75
79
  window.store.aiChatHistory = []
76
80
  }
77
81
 
82
+ function renderTabSelect () {
83
+ if (isAgent) {
84
+ return null
85
+ }
86
+ return (
87
+ <TabSelect
88
+ selectedTabIds={props.selectedTabIds}
89
+ tabs={props.tabs}
90
+ activeTabId={props.activeTabId}
91
+ />
92
+ )
93
+ }
94
+
78
95
  function renderSendIcon () {
79
96
  return (
80
97
  <SendOutlined
@@ -126,11 +143,13 @@ export default function AIChat (props) {
126
143
  />
127
144
  <Flex className='ai-chat-terminals' justify='space-between' align='center'>
128
145
  <Flex align='center'>
129
- <TabSelect
130
- selectedTabIds={props.selectedTabIds}
131
- tabs={props.tabs}
132
- activeTabId={props.activeTabId}
146
+ <Segmented
147
+ options={['Ask', 'Agent']}
148
+ value={mode === 'ask' ? 'Ask' : 'Agent'}
149
+ onChange={(val) => setMode(val === 'Ask' ? 'ask' : 'agent')}
150
+ size='small'
133
151
  />
152
+ {renderTabSelect()}
134
153
  <SettingOutlined
135
154
  onClick={toggleConfig}
136
155
  className='mg1l pointer icon-hover toggle-ai-setting-icon'
@@ -6,7 +6,7 @@ import {
6
6
  Alert,
7
7
  Space
8
8
  } from 'antd'
9
- import { useEffect } from 'react'
9
+ import { useEffect, useState } from 'react'
10
10
  import Link from '../common/external-link'
11
11
  import AiCache from './ai-cache'
12
12
  import {
@@ -14,6 +14,7 @@ import {
14
14
  } from '../../common/constants'
15
15
  import Password from '../common/password'
16
16
  import AiHistory, { addHistoryItem } from './ai-history'
17
+ import message from '../common/message'
17
18
 
18
19
  const STORAGE_KEY_CONFIG = 'ai_config_history'
19
20
  const EVENT_NAME_CONFIG = 'ai-config-history-update'
@@ -36,6 +37,7 @@ const proxyOptions = [
36
37
 
37
38
  export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
38
39
  const [form] = Form.useForm()
40
+ const [testing, setTesting] = useState(false)
39
41
  const baseURLAI = Form.useWatch('baseURLAI', form)
40
42
 
41
43
  useEffect(() => {
@@ -53,6 +55,37 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
53
55
  addHistoryItem(STORAGE_KEY_CONFIG, values, EVENT_NAME_CONFIG)
54
56
  }
55
57
 
58
+ const handleTest = async () => {
59
+ try {
60
+ const values = await form.validateFields()
61
+ setTesting(true)
62
+ const res = await window.pre.runGlobalAsync(
63
+ 'AIchat',
64
+ 'Hi',
65
+ values.modelAI,
66
+ values.roleAI,
67
+ values.baseURLAI,
68
+ values.apiPathAI,
69
+ values.apiKeyAI,
70
+ values.proxyAI,
71
+ false
72
+ )
73
+ if (res && res.error) {
74
+ message.error(res.error)
75
+ } else if (res && res.response) {
76
+ message.success('AI config works!')
77
+ } else {
78
+ message.error('Unexpected response from AI API')
79
+ }
80
+ } catch (e) {
81
+ if (e.message) {
82
+ message.error(e.message)
83
+ }
84
+ } finally {
85
+ setTesting(false)
86
+ }
87
+ }
88
+
56
89
  function handleSelectHistory (item) {
57
90
  if (item && typeof item === 'object') {
58
91
  form.setFieldsValue(item)
@@ -186,9 +219,17 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
186
219
  </Form.Item>
187
220
 
188
221
  <Form.Item>
189
- <Button type='primary' htmlType='submit'>
190
- {e('save')}
191
- </Button>
222
+ <Space>
223
+ <Button type='primary' htmlType='submit'>
224
+ {e('save')}
225
+ </Button>
226
+ <Button
227
+ loading={testing}
228
+ onClick={handleTest}
229
+ >
230
+ {e('testConnection')}
231
+ </Button>
232
+ </Space>
192
233
  </Form.Item>
193
234
  </Form>
194
235
  <AiHistory
@@ -77,3 +77,76 @@
77
77
  align-items center
78
78
  .ai-stop-icon-square
79
79
  margin-left auto
80
+
81
+ .agent-tool-calls
82
+ margin 8px 0
83
+
84
+ .agent-tool-call-card
85
+ border 1px solid var(--main-darker)
86
+ border-radius 4px
87
+ margin-bottom 6px
88
+ font-size 13px
89
+
90
+ .agent-tool-header
91
+ display flex
92
+ align-items center
93
+ padding 6px 8px
94
+ gap 6px
95
+
96
+ .agent-tool-name
97
+ font-weight 500
98
+
99
+ .agent-tool-tag
100
+ margin-left auto
101
+
102
+ .agent-tool-status-running
103
+ color #1890ff
104
+ animation spin 1s linear infinite
105
+
106
+ .agent-tool-status-completed
107
+ color #52c41a
108
+
109
+ .agent-tool-status-error
110
+ color #ff4d4f
111
+
112
+ .agent-tool-detail
113
+ padding 0 8px 8px
114
+ border-top 1px solid var(--main-darker)
115
+
116
+ .agent-tool-args
117
+ margin-top 6px
118
+
119
+ .agent-tool-result
120
+ margin-top 6px
121
+
122
+ .agent-tool-label
123
+ font-weight 500
124
+ margin-bottom 4px
125
+ font-size 12px
126
+ color var(--text-color-2)
127
+
128
+ .agent-tool-pre
129
+ background var(--main-dark)
130
+ padding 6px 8px
131
+ border-radius 3px
132
+ font-size 12px
133
+ max-height 200px
134
+ overflow-y auto
135
+ white-space pre-wrap
136
+ word-break break-all
137
+ margin-bottom 0
138
+
139
+ .agent-tool-running
140
+ border-color #1890ff
141
+
142
+ .agent-tool-completed
143
+ border-color #52c41a
144
+
145
+ .agent-tool-error
146
+ border-color #ff4d4f
147
+
148
+ @keyframes spin
149
+ from
150
+ transform rotate(0deg)
151
+ to
152
+ transform rotate(360deg)
@@ -81,6 +81,7 @@ const bookmarkSchema = {
81
81
  xon: 'boolean - enable XON flow control, default is false',
82
82
  xoff: 'boolean - enable XOFF flow control, default is false',
83
83
  xany: 'boolean - enable XANY flow control, default is false',
84
+ lineEnding: 'string - line ending for Enter key: "" (none), "\\r" (CR), "\\n" (LF), "\\r\\n" (CR+LF)',
84
85
  runScripts: 'array - run scripts after connected ({delay,script})',
85
86
  description: 'string - bookmark description'
86
87
  },