@electerm/electerm-react 5.3.15 → 5.5.6

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 (149) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +23 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +14 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/bookmark-form/index.jsx +10 -8
  29. package/client/components/common/item-filter.jsx +76 -0
  30. package/client/components/common/item-filter.styl +54 -0
  31. package/client/components/common/lazy-boundary.jsx +34 -0
  32. package/client/components/common/opacity.jsx +3 -1
  33. package/client/components/footer/footer-entry.jsx +35 -5
  34. package/client/components/footer/footer.styl +8 -1
  35. package/client/components/layout/layout.jsx +23 -31
  36. package/client/components/layout/pixed.js +3 -0
  37. package/client/components/main/main.jsx +80 -28
  38. package/client/components/main/term-fullscreen.styl +9 -3
  39. package/client/components/quick-commands/qm.styl +3 -2
  40. package/client/components/rdp/file-transfer.js +4 -4
  41. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  42. package/client/components/remote-monitor/monitor-details.styl +87 -0
  43. package/client/components/remote-monitor/monitor-model.js +764 -0
  44. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  45. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  46. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  47. package/client/components/remote-monitor/session-monitor.js +361 -0
  48. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  49. package/client/components/remote-monitor/visibility.js +18 -0
  50. package/client/components/session/session-control.jsx +29 -2
  51. package/client/components/session/session.jsx +40 -18
  52. package/client/components/session/sessions.jsx +5 -2
  53. package/client/components/setting-panel/col.jsx +32 -6
  54. package/client/components/setting-panel/list.styl +12 -0
  55. package/client/components/setting-panel/setting-common.jsx +25 -56
  56. package/client/components/setting-panel/setting-mobile-breadcrumb.jsx +29 -0
  57. package/client/components/setting-panel/setting-modal.jsx +64 -44
  58. package/client/components/setting-panel/setting-terminal.jsx +7 -2
  59. package/client/components/setting-panel/setting-wrap.jsx +0 -3
  60. package/client/components/setting-panel/setting-wrap.styl +80 -11
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  67. package/client/components/sidebar/index.jsx +28 -21
  68. package/client/components/sidebar/left-sidebar-icons.jsx +1 -1
  69. package/client/components/sidebar/sidebar.styl +16 -0
  70. package/client/components/sys-menu/menu-btn.jsx +30 -1
  71. package/client/components/tabs/index.jsx +2 -2
  72. package/client/components/tabs/tab-title.jsx +3 -2
  73. package/client/components/tabs/tab.jsx +5 -1
  74. package/client/components/tabs/tabs.styl +4 -0
  75. package/client/components/terminal/attach-addon-custom.js +295 -49
  76. package/client/components/terminal/automation/index.js +64 -0
  77. package/client/components/terminal/automation/keys.js +49 -0
  78. package/client/components/terminal/automation/stream-matcher.js +82 -0
  79. package/client/components/terminal/automation/strip-ansi.js +19 -0
  80. package/client/components/terminal/automation/trigger-engine.js +233 -0
  81. package/client/components/terminal/automation/trigger-presets.js +110 -0
  82. package/client/components/terminal/command-tracker-addon.js +31 -1
  83. package/client/components/terminal/mixins/index.js +27 -0
  84. package/client/components/terminal/mixins/term-attach.js +123 -0
  85. package/client/components/terminal/mixins/term-config.jsx +84 -0
  86. package/client/components/terminal/mixins/term-content.js +88 -0
  87. package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
  88. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  89. package/client/components/terminal/mixins/term-init.js +122 -0
  90. package/client/components/terminal/mixins/term-log.jsx +96 -0
  91. package/client/components/terminal/mixins/term-resize.js +87 -0
  92. package/client/components/terminal/mixins/term-socket.js +433 -0
  93. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  94. package/client/components/terminal/mixins/term-theme.js +190 -0
  95. package/client/components/terminal/mixins/term-touch.js +136 -0
  96. package/client/components/terminal/shell-detect.js +37 -0
  97. package/client/components/terminal/shell.js +0 -28
  98. package/client/components/terminal/shortcut-bar-entry.jsx +32 -27
  99. package/client/components/terminal/shortcut-bar.jsx +4 -6
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +56 -0
  102. package/client/components/terminal/startup-queue.js +379 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal.jsx +102 -2007
  105. package/client/components/terminal/transfer-client-base.js +5 -29
  106. package/client/components/terminal/trzsz-client.js +1 -3
  107. package/client/components/terminal/xmodem-client.js +1 -3
  108. package/client/components/terminal/xterm-loader.js +7 -0
  109. package/client/components/terminal/zmodem-client.js +1 -3
  110. package/client/components/terminal-info/base.jsx +18 -70
  111. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  112. package/client/components/terminal-info/terminal-info.jsx +50 -59
  113. package/client/components/terminal-info/terminal-info.styl +6 -49
  114. package/client/components/text-editor/open-text-editor.js +30 -0
  115. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  116. package/client/components/text-editor/text-editor.jsx +14 -0
  117. package/client/components/theme/theme-form.jsx +22 -0
  118. package/client/components/tree-list/bookmark-transport.jsx +7 -31
  119. package/client/components/tree-list/tree-list.styl +13 -0
  120. package/client/components/triggers/trigger-editor.jsx +246 -0
  121. package/client/components/triggers/trigger-form.jsx +200 -0
  122. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  123. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  124. package/client/components/triggers/trigger-transport.jsx +60 -0
  125. package/client/components/triggers/triggers-field.jsx +19 -0
  126. package/client/components/widgets/widgets-list.jsx +11 -1
  127. package/client/css/mobile.styl +9 -1
  128. package/client/store/common.js +12 -1
  129. package/client/store/init-state.js +14 -0
  130. package/client/store/mcp-handler.js +158 -30
  131. package/client/store/setting.js +20 -0
  132. package/client/store/sidebar.js +12 -0
  133. package/client/store/store.js +2 -0
  134. package/client/store/sync.js +45 -9
  135. package/client/store/system-menu.js +2 -0
  136. package/client/store/tab.js +41 -3
  137. package/client/store/trigger.js +118 -0
  138. package/client/store/watch.js +13 -5
  139. package/client/views/index.pug +3 -0
  140. package/package.json +1 -1
  141. package/client/components/terminal-info/activity.jsx +0 -57
  142. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  143. package/client/components/terminal-info/disk.jsx +0 -45
  144. package/client/components/terminal-info/format-disks.js +0 -45
  145. package/client/components/terminal-info/network.jsx +0 -121
  146. package/client/components/terminal-info/resource.jsx +0 -99
  147. package/client/components/terminal-info/run-cmd.jsx +0 -263
  148. package/client/components/terminal-info/sys-info.jsx +0 -56
  149. package/client/components/terminal-info/up.jsx +0 -17
@@ -1,11 +1,20 @@
1
- import { lazy, Suspense } from 'react'
1
+ import { lazy, Suspense, useState } from 'react'
2
+ import LazyBoundary from '../common/lazy-boundary'
2
3
 
3
4
  const AIChat = lazy(() => import('./ai-chat'))
4
5
 
5
6
  export default function AIChatEntry (props) {
7
+ const [activated, setActivated] = useState(false)
8
+ const active = props.rightPanelTab === 'ai'
9
+ if (active && !activated) setActivated(true)
10
+ // Keep drafts and attachments when switching to info after opening chat.
11
+ if (!active && !activated) return null
12
+
6
13
  return (
7
- <Suspense fallback={null}>
8
- <AIChat {...props} />
9
- </Suspense>
14
+ <LazyBoundary>
15
+ <Suspense fallback={null}>
16
+ <AIChat {...props} />
17
+ </Suspense>
18
+ </LazyBoundary>
10
19
  )
11
20
  }
@@ -6,15 +6,18 @@ import { runAgentLoop } from './agent'
6
6
  import { appendMandatoryGuardrails } from './ai-guardrails'
7
7
  import {
8
8
  Alert,
9
- Tooltip
9
+ Tooltip,
10
+ Tag
10
11
  } from 'antd'
11
12
  import {
12
13
  CopyOutlined,
13
14
  CloseOutlined,
14
15
  CaretDownOutlined,
15
- CaretRightOutlined
16
+ CaretRightOutlined,
17
+ PaperClipOutlined
16
18
  } from '@ant-design/icons'
17
19
  import { copy } from '../../common/clipboard'
20
+ import { formatSize } from './ai-attachments'
18
21
 
19
22
  export default function AIChatHistoryItem ({ item }) {
20
23
  const [showOutput, setShowOutput] = useState(true)
@@ -81,7 +84,12 @@ export default function AIChatHistoryItem ({ item }) {
81
84
  content: 'Understood. I will use this context as we continue.'
82
85
  })
83
86
  } else {
84
- messages.push({ role: 'user', content: entry.prompt })
87
+ messages.push({
88
+ role: 'user',
89
+ // promptWithAttachments carries the inlined file blocks;
90
+ // falls back to the plain prompt for legacy entries
91
+ content: entry.promptWithAttachments || entry.prompt
92
+ })
85
93
  if (entry.response && entry.id !== item.id) {
86
94
  messages.push({ role: 'assistant', content: entry.response })
87
95
  }
@@ -271,6 +279,21 @@ export default function AIChatHistoryItem ({ item }) {
271
279
  )
272
280
  }
273
281
 
282
+ function renderAttachments () {
283
+ if (!item.attachments || !item.attachments.length) {
284
+ return null
285
+ }
286
+ return (
287
+ <div className='ai-chat-item-attachments mg1b'>
288
+ {item.attachments.map(a => (
289
+ <Tag key={a.name + a.size} title={`${a.name} (${formatSize(a.size)})`}>
290
+ <PaperClipOutlined /> {a.name}
291
+ </Tag>
292
+ ))}
293
+ </div>
294
+ )
295
+ }
296
+
274
297
  function renderToolCalls () {
275
298
  if (mode !== 'agent' || !toolCalls || !toolCalls.length) {
276
299
  return null
@@ -278,7 +301,7 @@ export default function AIChatHistoryItem ({ item }) {
278
301
  return (
279
302
  <div className='agent-tool-calls'>
280
303
  {toolCalls.map((tc) => (
281
- <AgentToolCallCard key={tc.id} toolCall={tc} />
304
+ <AgentToolCallCard key={tc.id} toolCall={tc} autoCollapse={!isStreaming} />
282
305
  ))}
283
306
  </div>
284
307
  )
@@ -291,6 +314,7 @@ export default function AIChatHistoryItem ({ item }) {
291
314
  <Alert {...alertProps} />
292
315
  </Tooltip>
293
316
  </div>
317
+ {renderAttachments()}
294
318
  {renderToolCalls()}
295
319
  {showOutput && <AIOutput item={item} />}
296
320
  {renderStopButton()}
@@ -2,6 +2,7 @@
2
2
  import { useLayoutEffect, useRef } from 'react'
3
3
  import { auto } from 'manate/react'
4
4
  import AIChatHistoryItem from './ai-chat-history-item'
5
+ import AiChatEmpty from './ai-chat-empty'
5
6
 
6
7
  export default auto(function AIChatHistory ({ history }) {
7
8
  const historyRef = useRef(null)
@@ -12,7 +13,7 @@ export default auto(function AIChatHistory ({ history }) {
12
13
  }
13
14
  }, [history])
14
15
  if (!history.length) {
15
- return <div />
16
+ return <AiChatEmpty />
16
17
  }
17
18
  return (
18
19
  <div ref={historyRef} className='ai-history-wrap'>
@@ -1,5 +1,5 @@
1
- import { useState, useCallback, useEffect } from 'react'
2
- import { Flex, Input, Segmented, Button } from 'antd'
1
+ import { useState, useCallback, useEffect, useRef } from 'react'
2
+ import { Flex, Input, Segmented, Button, Tag, message } from 'antd'
3
3
  import TabSelect from '../footer/tab-select'
4
4
  import AiChatHistory from './ai-chat-history'
5
5
  import AiChatSessions from './ai-chat-sessions'
@@ -10,7 +10,8 @@ import {
10
10
  SendOutlined,
11
11
  PlusOutlined,
12
12
  HistoryOutlined,
13
- CompressOutlined
13
+ CompressOutlined,
14
+ PaperClipOutlined
14
15
  } from '@ant-design/icons'
15
16
  import {
16
17
  aiConfigWikiLink,
@@ -19,6 +20,12 @@ import {
19
20
  import { getItem, setItem } from '../../common/safe-local-storage.js'
20
21
  import HelpIcon from '../common/help-icon'
21
22
  import { refsStatic } from '../common/ref'
23
+ import {
24
+ readTextAttachment,
25
+ validateAttachmentTotal,
26
+ buildAttachmentsBlock,
27
+ formatSize
28
+ } from './ai-attachments'
22
29
  import './ai.styl'
23
30
 
24
31
  const { TextArea } = Input
@@ -27,6 +34,9 @@ const MAX_HISTORY = 500
27
34
  export default function AIChat (props) {
28
35
  const [prompt, setPrompt] = useState('')
29
36
  const [compressing, setCompressing] = useState(false)
37
+ const [attachments, setAttachments] = useState([])
38
+ const [dragOver, setDragOver] = useState(false)
39
+ const fileInputRef = useRef(null)
30
40
  const [mode, setMode] = useState(() => getItem(aiChatModeLsKey) || 'ask')
31
41
  const isAgent = mode === 'agent'
32
42
  const submitDisabled = isAgent && props.agentRunning
@@ -53,15 +63,55 @@ export default function AIChat (props) {
53
63
  setMode(m)
54
64
  }
55
65
 
66
+ const e = window.translate
67
+
68
+ async function handleFiles (files) {
69
+ const notText = []
70
+ let totalTooLarge = false
71
+ const next = [...attachments]
72
+ for (const file of Array.from(files)) {
73
+ if (next.some(a => a.name === file.name && a.size === file.size)) {
74
+ continue
75
+ }
76
+ if (!validateAttachmentTotal(next, file.size)) {
77
+ totalTooLarge = true
78
+ continue
79
+ }
80
+ const { attachment, error } = await readTextAttachment(file)
81
+ if (error === 'notTextFile') {
82
+ notText.push(file.name)
83
+ continue
84
+ }
85
+ if (attachment) {
86
+ next.push(attachment)
87
+ }
88
+ }
89
+ setAttachments(next)
90
+ if (totalTooLarge) {
91
+ message.error('attachment too big')
92
+ }
93
+ if (notText.length) {
94
+ message.error(`attachment only support text files: ${notText.join(', ')}`)
95
+ }
96
+ }
97
+
56
98
  const handleSubmit = useCallback(function () {
57
99
  if (window.store.aiConfigMissing()) {
58
100
  window.store.toggleAIConfig()
59
101
  }
60
- if (!prompt.trim()) return
102
+ if (!prompt.trim() && !attachments.length) return
61
103
 
62
104
  const chatId = uid()
105
+ const block = buildAttachmentsBlock(attachments)
106
+ const promptWithAttachments = block
107
+ ? (prompt.trim() ? prompt + '\n\n' + block : block)
108
+ : prompt
63
109
  const chatEntry = {
64
110
  prompt,
111
+ promptWithAttachments,
112
+ attachments: attachments.map(({ name, size, truncated }) => ({
113
+ name, size, truncated
114
+ })),
65
115
  response: '',
66
116
  isStreaming: false,
67
117
  pending: true,
@@ -86,11 +136,12 @@ export default function AIChat (props) {
86
136
 
87
137
  window.store.aiChatHistory.push(chatEntry)
88
138
  setPrompt('')
139
+ setAttachments([])
89
140
 
90
141
  if (window.store.aiChatHistory.length > MAX_HISTORY) {
91
142
  window.store.aiChatHistory.splice(MAX_HISTORY)
92
143
  }
93
- }, [prompt, mode, currentChatSessionId])
144
+ }, [prompt, attachments, mode, currentChatSessionId])
94
145
 
95
146
  function renderHistory () {
96
147
  if (props.showChatSessions) {
@@ -168,9 +219,6 @@ export default function AIChat (props) {
168
219
  setPrompt,
169
220
  handleSubmit
170
221
  })
171
- if (props.rightPanelTab === 'ai' && window.store.aiConfigMissing()) {
172
- window.store.toggleAIConfig()
173
- }
174
222
  return () => {
175
223
  refsStatic.remove('AIChat')
176
224
  }
@@ -180,17 +228,32 @@ export default function AIChat (props) {
180
228
  return null
181
229
  }
182
230
 
183
- const handleKeyPress = (e) => {
184
- if (!e.shiftKey) {
185
- e.preventDefault()
231
+ const handleKeyPress = (evt) => {
232
+ if (!evt.shiftKey) {
233
+ evt.preventDefault()
186
234
  if (!submitDisabled) {
187
235
  handleSubmit()
188
236
  }
189
237
  }
190
238
  }
191
- const e = window.translate
192
239
  return (
193
- <Flex vertical className='ai-chat-container'>
240
+ <Flex
241
+ vertical
242
+ className={'ai-chat-container' + (dragOver ? ' ai-attachment-dragover' : '')}
243
+ onDragOver={(evt) => {
244
+ evt.preventDefault()
245
+ setDragOver(true)
246
+ }}
247
+ onDragLeave={() => setDragOver(false)}
248
+ onDrop={(evt) => {
249
+ setDragOver(false)
250
+ if (!evt.dataTransfer.files.length) {
251
+ return
252
+ }
253
+ evt.preventDefault()
254
+ handleFiles(evt.dataTransfer.files)
255
+ }}
256
+ >
194
257
  <Flex className='ai-chat-history' flex='auto'>
195
258
  {renderHistory()}
196
259
  </Flex>
@@ -225,6 +288,20 @@ export default function AIChat (props) {
225
288
  {e('history')}
226
289
  </Button>
227
290
  </Flex>
291
+ {attachments.length > 0 && (
292
+ <Flex wrap='wrap' gap={4} className='ai-chat-attachments mg1b'>
293
+ {attachments.map(a => (
294
+ <Tag
295
+ key={a.id}
296
+ closable
297
+ onClose={() => setAttachments(arr => arr.filter(x => x.id !== a.id))}
298
+ title={`${a.name} (${formatSize(a.size)})`}
299
+ >
300
+ <PaperClipOutlined /> {a.name}
301
+ </Tag>
302
+ ))}
303
+ </Flex>
304
+ )}
228
305
  <TextArea
229
306
  value={prompt}
230
307
  onChange={handlePromptChange}
@@ -233,6 +310,16 @@ export default function AIChat (props) {
233
310
  autoSize={{ minRows: 3, maxRows: 10 }}
234
311
  className='ai-chat-textarea'
235
312
  />
313
+ <input
314
+ type='file'
315
+ multiple
316
+ ref={fileInputRef}
317
+ style={{ display: 'none' }}
318
+ onChange={(evt) => {
319
+ handleFiles(evt.target.files)
320
+ evt.target.value = ''
321
+ }}
322
+ />
236
323
  <Flex className='ai-chat-terminals' justify='space-between' align='center'>
237
324
  <Flex align='center'>
238
325
  <Segmented
@@ -242,6 +329,10 @@ export default function AIChat (props) {
242
329
  size='small'
243
330
  />
244
331
  {renderTabSelect()}
332
+ <PaperClipOutlined
333
+ onClick={() => fileInputRef.current?.click()}
334
+ className='mg1l pointer icon-hover toggle-ai-attach-icon'
335
+ />
245
336
  <SettingOutlined
246
337
  onClick={toggleConfig}
247
338
  className='mg1l pointer icon-hover toggle-ai-setting-icon'
@@ -10,133 +10,7 @@ export const aiConfigsArr = [
10
10
  'proxyAI'
11
11
  ]
12
12
 
13
- export const defaultAIPresets = [
14
- {
15
- id: 'atlascloud',
16
- nameAI: 'AtlasCloud',
17
- baseURLAI: 'https://api.atlascloud.ai/v1',
18
- apiPathAI: '/chat/completions',
19
- modelAI: 'deepseek-ai/deepseek-v4-flash',
20
- authHeaderNameAI: 'Authorization: Bearer'
21
- },
22
- {
23
- id: 'ApiSmart',
24
- nameAI: 'ApiSmart',
25
- baseURLAI: 'https://api.apismart.ai/v1',
26
- apiPathAI: '/chat/completions',
27
- modelAI: 'DEEPSEEK_V4_FLASH',
28
- authHeaderNameAI: 'Authorization: Bearer'
29
- },
30
- {
31
- id: 'apimart',
32
- nameAI: 'ApiMart',
33
- baseURLAI: 'https://api.apimart.ai/v1',
34
- apiPathAI: '/chat/completions',
35
- modelAI: 'deepseek-v4-flash',
36
- authHeaderNameAI: 'Authorization: Bearer'
37
- },
38
- {
39
- id: 'deepseek',
40
- nameAI: 'DeepSeek',
41
- baseURLAI: 'https://api.deepseek.com/v1',
42
- apiPathAI: '/chat/completions',
43
- modelAI: 'deepseek-chat',
44
- authHeaderNameAI: 'Authorization: Bearer'
45
- },
46
- {
47
- id: 'openai',
48
- nameAI: 'OpenAI',
49
- baseURLAI: 'https://api.openai.com/v1',
50
- apiPathAI: '/chat/completions',
51
- modelAI: 'gpt-4o',
52
- authHeaderNameAI: 'Authorization: Bearer'
53
- },
54
- {
55
- id: 'openrouter',
56
- nameAI: 'OpenRouter',
57
- baseURLAI: 'https://openrouter.ai/api/v1',
58
- apiPathAI: '/chat/completions',
59
- modelAI: 'openai/gpt-4o-mini',
60
- authHeaderNameAI: 'Authorization: Bearer'
61
- },
62
- {
63
- id: 'orcarouter',
64
- nameAI: 'OrcaRouter',
65
- baseURLAI: 'https://api.orcarouter.ai/v1',
66
- apiPathAI: '/chat/completions',
67
- modelAI: 'deepseek/deepseek-v4-flash',
68
- authHeaderNameAI: 'Authorization: Bearer'
69
- },
70
- {
71
- id: 'google',
72
- nameAI: 'Google Gemini',
73
- baseURLAI: 'https://generativelanguage.googleapis.com/v1beta/openai',
74
- apiPathAI: '/chat/completions',
75
- modelAI: 'gemini-2.0-flash',
76
- authHeaderNameAI: 'Authorization: Bearer'
77
- },
78
- {
79
- id: 'groq',
80
- nameAI: 'Groq',
81
- baseURLAI: 'https://api.groq.com/openai/v1',
82
- apiPathAI: '/chat/completions',
83
- modelAI: 'llama-3.3-70b-versatile',
84
- authHeaderNameAI: 'Authorization: Bearer'
85
- },
86
- {
87
- id: 'together',
88
- nameAI: 'Together AI',
89
- baseURLAI: 'https://api.together.xyz/v1',
90
- apiPathAI: '/chat/completions',
91
- modelAI: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
92
- authHeaderNameAI: 'Authorization: Bearer'
93
- },
94
- {
95
- id: 'mistral',
96
- nameAI: 'Mistral AI',
97
- baseURLAI: 'https://api.mistral.ai/v1',
98
- apiPathAI: '/chat/completions',
99
- modelAI: 'mistral-large-latest',
100
- authHeaderNameAI: 'Authorization: Bearer'
101
- },
102
- {
103
- id: 'xai',
104
- nameAI: 'xAI (Grok)',
105
- baseURLAI: 'https://api.x.ai/v1',
106
- apiPathAI: '/chat/completions',
107
- modelAI: 'grok-3-mini',
108
- authHeaderNameAI: 'Authorization: Bearer'
109
- },
110
- {
111
- id: 'perplexity',
112
- nameAI: 'Perplexity',
113
- baseURLAI: 'https://api.perplexity.ai',
114
- apiPathAI: '/chat/completions',
115
- modelAI: 'sonar',
116
- authHeaderNameAI: 'Authorization: Bearer'
117
- },
118
- {
119
- id: 'moonshot',
120
- nameAI: 'Moonshot (Kimi)',
121
- baseURLAI: 'https://api.moonshot.cn/v1',
122
- apiPathAI: '/chat/completions',
123
- modelAI: 'moonshot-v1-8k',
124
- authHeaderNameAI: 'Authorization: Bearer'
125
- },
126
- {
127
- id: 'siliconflow',
128
- nameAI: 'SiliconFlow',
129
- baseURLAI: 'https://api.siliconflow.cn/v1',
130
- apiPathAI: '/chat/completions',
131
- modelAI: 'deepseek-ai/DeepSeek-V3',
132
- authHeaderNameAI: 'Authorization: Bearer'
133
- }
134
- ]
135
-
136
- export function getAIPresets () {
137
- const presets = [...defaultAIPresets]
138
- if (window.et?.defaultAIPreset) {
139
- presets.unshift(window.et.defaultAIPreset)
140
- }
141
- return presets
142
- }
13
+ export {
14
+ defaultAIPresets,
15
+ getAIPresets
16
+ } from './ai-presets'
@@ -7,8 +7,8 @@ import {
7
7
  Space,
8
8
  Dropdown
9
9
  } from 'antd'
10
- import { useEffect, useState } from 'react'
11
- import { DownOutlined } from '@ant-design/icons'
10
+ import { useEffect, useMemo, useState } from 'react'
11
+ import { DownOutlined, ReloadOutlined } from '@ant-design/icons'
12
12
  import Link from '../common/external-link'
13
13
  import AiCache from './ai-cache'
14
14
  import {
@@ -17,11 +17,50 @@ import {
17
17
  import Password from '../common/password'
18
18
  import AiHistory, { addHistoryItem } from './ai-history'
19
19
  import message from '../common/message'
20
- import { getAIPresets } from './ai-config-props'
20
+ import { getAIPresets } from './ai-presets'
21
21
  import { appendMandatoryGuardrails } from './ai-guardrails'
22
22
 
23
23
  const STORAGE_KEY_CONFIG = 'ai_config_history'
24
24
  const EVENT_NAME_CONFIG = 'ai-config-history-update'
25
+ const STORAGE_KEY_MODELS = 'ai_models_cache'
26
+
27
+ // Model lists fetched from `<baseURL>/models`, cached per API URL
28
+ function normalizeBaseURL (url) {
29
+ return (url || '').trim().replace(/\/+$/, '')
30
+ }
31
+
32
+ function readModelsCache () {
33
+ try {
34
+ const raw = window.localStorage.getItem(STORAGE_KEY_MODELS)
35
+ const parsed = raw ? JSON.parse(raw) : null
36
+ return parsed && typeof parsed === 'object' ? parsed : {}
37
+ } catch (e) {
38
+ return {}
39
+ }
40
+ }
41
+
42
+ function getCachedModels (baseURL) {
43
+ const key = normalizeBaseURL(baseURL)
44
+ if (!key) {
45
+ return []
46
+ }
47
+ const models = readModelsCache()[key]
48
+ return Array.isArray(models) ? models : []
49
+ }
50
+
51
+ function saveModelsToCache (baseURL, models) {
52
+ const key = normalizeBaseURL(baseURL)
53
+ if (!key) {
54
+ return
55
+ }
56
+ try {
57
+ const cache = readModelsCache()
58
+ cache[key] = models
59
+ window.localStorage.setItem(STORAGE_KEY_MODELS, JSON.stringify(cache))
60
+ } catch (e) {
61
+ // storage full or disabled, keep it in memory only
62
+ }
63
+ }
25
64
 
26
65
  const e = window.translate
27
66
  const defaultRoles = [
@@ -39,6 +78,15 @@ const proxyOptions = [
39
78
  { value: 'https://proxy.example.com:3128' }
40
79
  ]
41
80
 
81
+ // The wire protocol is detected from the path:
82
+ // /chat/completions -> OpenAI compatible, /responses -> OpenAI Responses API,
83
+ // /messages -> Anthropic Claude Messages API
84
+ const apiPathOptions = [
85
+ { value: '/chat/completions', label: '/chat/completions (OpenAI)' },
86
+ { value: '/responses', label: '/responses (OpenAI Responses)' },
87
+ { value: '/messages', label: '/messages (Anthropic)' }
88
+ ]
89
+
42
90
  const authHeaderOptions = [
43
91
  { value: 'Authorization: Bearer' },
44
92
  { value: 'x-api-key' },
@@ -50,7 +98,11 @@ const authHeaderOptions = [
50
98
  export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
51
99
  const [form] = Form.useForm()
52
100
  const [testing, setTesting] = useState(false)
101
+ const [loadingModels, setLoadingModels] = useState(false)
102
+ const [cachedModels, setCachedModels] = useState([])
53
103
  const baseURLAI = Form.useWatch('baseURLAI', form)
104
+ const presets = useMemo(() => getAIPresets(), [])
105
+ const currentPreset = presets.find(p => p.baseURLAI === baseURLAI)
54
106
 
55
107
  useEffect(() => {
56
108
  if (initialValues) {
@@ -58,6 +110,11 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
58
110
  }
59
111
  }, [initialValues])
60
112
 
113
+ // Restore the model list previously fetched for this API URL
114
+ useEffect(() => {
115
+ setCachedModels(getCachedModels(baseURLAI))
116
+ }, [baseURLAI])
117
+
61
118
  function filter () {
62
119
  return true
63
120
  }
@@ -105,8 +162,45 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
105
162
  }
106
163
  }
107
164
 
165
+ async function handleLoadModels () {
166
+ const values = form.getFieldsValue()
167
+ const baseURL = normalizeBaseURL(values.baseURLAI)
168
+ if (!baseURL) {
169
+ message.error('Please input API URL first')
170
+ return
171
+ }
172
+ setLoadingModels(true)
173
+ try {
174
+ const res = await window.pre.runGlobalAsync(
175
+ 'AIlistModels',
176
+ baseURL,
177
+ values.apiKeyAI,
178
+ values.authHeaderNameAI,
179
+ values.proxyAI
180
+ )
181
+ if (res && res.error) {
182
+ message.error(res.error)
183
+ } else if (res && res.models && res.models.length) {
184
+ setCachedModels(res.models)
185
+ saveModelsToCache(baseURL, res.models)
186
+ message.success(`Got ${res.models.length} models`)
187
+ if (!values.modelAI) {
188
+ form.setFieldsValue({ modelAI: res.models[0] })
189
+ }
190
+ } else {
191
+ message.error('No models found in response')
192
+ }
193
+ } catch (e) {
194
+ if (e.message) {
195
+ message.error(e.message)
196
+ }
197
+ } finally {
198
+ setLoadingModels(false)
199
+ }
200
+ }
201
+
108
202
  function handleSelectPreset (preset) {
109
- const fields = ['nameAI', 'baseURLAI', 'apiPathAI', 'modelAI', 'authHeaderNameAI', 'modelAI', 'apiKeyAI']
203
+ const fields = ['nameAI', 'baseURLAI', 'apiPathAI', 'modelAI', 'authHeaderNameAI', 'apiKeyAI']
110
204
  const values = {}
111
205
  fields.forEach(f => {
112
206
  if (preset[f] !== undefined) {
@@ -117,7 +211,6 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
117
211
  }
118
212
 
119
213
  function renderPresetMenu () {
120
- const presets = getAIPresets()
121
214
  const items = presets.map(p => ({
122
215
  key: p.id,
123
216
  label: p.nameAI,
@@ -126,7 +219,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
126
219
  return (
127
220
  <Dropdown menu={{ items }} trigger={['click']}>
128
221
  <Button>
129
- {e('presets') || 'Presets'} <DownOutlined />
222
+ {e('presets')} <DownOutlined />
130
223
  </Button>
131
224
  </Dropdown>
132
225
  )
@@ -145,15 +238,41 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
145
238
  }
146
239
 
147
240
  function renderApiKeyLabel () {
148
- if (baseURLAI === 'https://api.atlascloud.ai/v1') {
149
- return <span className='bold'>API Key (<Link to='https://www.atlascloud.ai/?utm_source=electerm_app&utm_medium=link&utm_campaign=electerm'>get API key from atlascloud</Link>)</span>
150
- }
151
- if (baseURLAI === 'https://ai.electerm.org/api/ai') {
152
- return <span className='bold'>API Key (<Link to='https://ai.electerm.org?utm=electerm'>get API key from ai.electerm.org(free)</Link>)</span>
241
+ const siteUrl = currentPreset?.siteUrl
242
+ if (siteUrl) {
243
+ const name = currentPreset?.nameAI || baseURLAI
244
+ return <span className='bold'>API Key (<Link to={siteUrl}>get API key from {name}</Link>)</span>
153
245
  }
154
246
  return 'API Key'
155
247
  }
156
248
 
249
+ function renderModelInput () {
250
+ const presetModels = (currentPreset?.modelAIs || [])
251
+ .map(o => (typeof o === 'string' ? o : o.value))
252
+ const values = [...presetModels, ...cachedModels].filter(Boolean)
253
+ const options = [...new Set(values)].map(value => ({ value }))
254
+ const title = 'Fetch model list from API URL'
255
+ return (
256
+ <AutoComplete
257
+ options={options}
258
+ filterOption={filter}
259
+ >
260
+ <Input
261
+ placeholder='Enter or select AI model'
262
+ suffix={
263
+ <ReloadOutlined
264
+ spin={loadingModels}
265
+ className='pointer ai-model-reload'
266
+ title={title}
267
+ onMouseDown={e => e.preventDefault()}
268
+ onClick={handleLoadModels}
269
+ />
270
+ }
271
+ />
272
+ </AutoComplete>
273
+ )
274
+ }
275
+
157
276
  if (!showAIConfig) {
158
277
  return null
159
278
  }
@@ -219,8 +338,11 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
219
338
  ]}
220
339
  noStyle
221
340
  >
222
- <Input
341
+ <AutoComplete
342
+ options={apiPathOptions}
343
+ filterOption={filter}
223
344
  placeholder='/chat/completions'
345
+ popupMatchSelectWidth={false}
224
346
  style={{ width: '25%' }}
225
347
  />
226
348
  </Form.Item>
@@ -231,9 +353,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
231
353
  name='modelAI'
232
354
  rules={[{ required: true, message: 'Please input or select a model!' }]}
233
355
  >
234
- <Input
235
- placeholder='Enter or select AI model'
236
- />
356
+ {renderModelInput()}
237
357
  </Form.Item>
238
358
 
239
359
  <Form.Item