@electerm/electerm-react 3.15.126 → 3.15.128
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/components/ai/ai-config.jsx +8 -5
- package/client/components/shortcuts/shortcuts-defaults.js +1 -1
- package/client/components/terminal/attach-addon-custom.js +7 -1
- package/client/components/terminal/terminal-command-dropdown.jsx +22 -7
- package/client/components/terminal/terminal.jsx +32 -5
- package/package.json +1 -1
|
@@ -143,11 +143,14 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
143
143
|
return { label, title }
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function
|
|
146
|
+
function renderApiKeyLabel () {
|
|
147
147
|
if (baseURLAI === 'https://api.atlascloud.ai/v1') {
|
|
148
|
-
return <span>API
|
|
148
|
+
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>
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
if (baseURLAI === 'https://ai.electerm.org/api/ai') {
|
|
151
|
+
return <span className='bold'>API Key (<Link to='https://ai.electerm.org?utm=electerm'>get API key from ai.electerm.org(free)</Link>)</span>
|
|
152
|
+
}
|
|
153
|
+
return 'API Key'
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
if (!showAIConfig) {
|
|
@@ -184,7 +187,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
184
187
|
placeholder='e.g. DeepSeek Relay, Local Ollama (optional)'
|
|
185
188
|
/>
|
|
186
189
|
</Form.Item>
|
|
187
|
-
<Form.Item label=
|
|
190
|
+
<Form.Item label='API URL' required>
|
|
188
191
|
<Space.Compact className='width-100'>
|
|
189
192
|
<Form.Item
|
|
190
193
|
label='API URL'
|
|
@@ -226,7 +229,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
226
229
|
</Form.Item>
|
|
227
230
|
|
|
228
231
|
<Form.Item
|
|
229
|
-
label=
|
|
232
|
+
label={renderApiKeyLabel()}
|
|
230
233
|
name='apiKeyAI'
|
|
231
234
|
>
|
|
232
235
|
<Password placeholder='Enter your API key' />
|
|
@@ -224,7 +224,12 @@ export default class AttachAddonCustom {
|
|
|
224
224
|
|
|
225
225
|
if (typeof data === 'string') {
|
|
226
226
|
term?.parent?.notifyOnData()
|
|
227
|
-
|
|
227
|
+
term.write(data)
|
|
228
|
+
// Notify parent that the terminal buffer has been updated (echo received).
|
|
229
|
+
// This allows the suggestion dropdown to refresh after the buffer reflects
|
|
230
|
+
// the latest server-side state (e.g., after backspace echo).
|
|
231
|
+
term?.parent?.onTerminalWrite?.()
|
|
232
|
+
return
|
|
228
233
|
}
|
|
229
234
|
data = new Uint8Array(data)
|
|
230
235
|
const fileReader = new FileReader()
|
|
@@ -238,6 +243,7 @@ export default class AttachAddonCustom {
|
|
|
238
243
|
term?.parent?.notifyOnData()
|
|
239
244
|
const str = this.decoder.decode(data)
|
|
240
245
|
term?.write(str)
|
|
246
|
+
term?.parent?.onTerminalWrite?.()
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
sendToServer = (data) => {
|
|
@@ -2,7 +2,6 @@ import { Component } from 'manate/react/class-components'
|
|
|
2
2
|
import { refsStatic, refs } from '../common/ref'
|
|
3
3
|
import SuggestionItem from './cmd-item'
|
|
4
4
|
import { aiSuggestionsCache } from '../../common/cache'
|
|
5
|
-
import uid from '../../common/uid'
|
|
6
5
|
import classnames from 'classnames'
|
|
7
6
|
import {
|
|
8
7
|
LoadingOutlined
|
|
@@ -130,6 +129,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
130
129
|
} else {
|
|
131
130
|
position.top = top + cellHeight
|
|
132
131
|
}
|
|
132
|
+
this._suggestionsCache = null
|
|
133
133
|
this.setState({
|
|
134
134
|
showSuggestions: true,
|
|
135
135
|
cursorPosition: position,
|
|
@@ -188,6 +188,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
188
188
|
window.store.addCmdHistory(item.command, 'aiCmdHistory')
|
|
189
189
|
})
|
|
190
190
|
}
|
|
191
|
+
this._suggestionsCache = null
|
|
191
192
|
this.setState({
|
|
192
193
|
showSuggestions: false,
|
|
193
194
|
aiSuggestions: [],
|
|
@@ -261,7 +262,8 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
261
262
|
if (!uniqueCommands.has(command)) {
|
|
262
263
|
uniqueCommands.add(command)
|
|
263
264
|
res.push({
|
|
264
|
-
|
|
265
|
+
// Use stable key to avoid React re-mounting items on every render
|
|
266
|
+
id: type + ':' + command,
|
|
265
267
|
command,
|
|
266
268
|
type
|
|
267
269
|
})
|
|
@@ -277,7 +279,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
277
279
|
if (b.password && !seen.has(b.password)) {
|
|
278
280
|
seen.add(b.password)
|
|
279
281
|
res.push({
|
|
280
|
-
id:
|
|
282
|
+
id: 'PW:' + b.password,
|
|
281
283
|
command: b.password,
|
|
282
284
|
type: 'PW',
|
|
283
285
|
hint: [b.username, [b.host, b.port].filter(Boolean).join(':')].filter(Boolean).join('@')
|
|
@@ -287,28 +289,41 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
287
289
|
return this.state.reverse ? res.reverse() : res
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
_suggestionsCache = null
|
|
293
|
+
_suggestionsCacheKey = ''
|
|
294
|
+
|
|
290
295
|
getSuggestions = () => {
|
|
296
|
+
// Memoize: only recompute when cmd, reverse, aiSuggestions, or props change
|
|
297
|
+
const { cmd, reverse, aiSuggestions } = this.state
|
|
298
|
+
const { suggestions } = this.props
|
|
299
|
+
const cacheKey = cmd + '|' + reverse + '|' + (aiSuggestions?.length || 0) + '|' + (suggestions?.history?.length || 0) + '|' + (suggestions?.batch?.length || 0) + '|' + (suggestions?.quick?.length || 0)
|
|
300
|
+
if (this._suggestionsCache && this._suggestionsCacheKey === cacheKey) {
|
|
301
|
+
return this._suggestionsCache
|
|
302
|
+
}
|
|
291
303
|
const uniqueCommands = new Set()
|
|
292
304
|
const {
|
|
293
305
|
history = [],
|
|
294
306
|
batch = [],
|
|
295
307
|
quick = []
|
|
296
|
-
} =
|
|
308
|
+
} = suggestions || {}
|
|
297
309
|
const res = []
|
|
298
|
-
|
|
310
|
+
aiSuggestions
|
|
299
311
|
.forEach(item => {
|
|
300
312
|
if (!uniqueCommands.has(item.command)) {
|
|
301
313
|
uniqueCommands.add(item.command)
|
|
302
314
|
}
|
|
303
315
|
res.push({
|
|
304
|
-
id:
|
|
316
|
+
id: 'AI:' + item.command,
|
|
305
317
|
...item
|
|
306
318
|
})
|
|
307
319
|
})
|
|
308
320
|
this.processCommands(history, 'H', uniqueCommands, res)
|
|
309
321
|
this.processCommands(batch, 'B', uniqueCommands, res)
|
|
310
322
|
this.processCommands(quick, 'Q', uniqueCommands, res)
|
|
311
|
-
|
|
323
|
+
const finalRes = reverse ? res.reverse() : res
|
|
324
|
+
this._suggestionsCache = finalRes
|
|
325
|
+
this._suggestionsCacheKey = cacheKey
|
|
326
|
+
return finalRes
|
|
312
327
|
}
|
|
313
328
|
|
|
314
329
|
renderAIIcon () {
|
|
@@ -1081,18 +1081,45 @@ class Term extends Component {
|
|
|
1081
1081
|
return
|
|
1082
1082
|
}
|
|
1083
1083
|
if (this.props.config.showCmdSuggestions) {
|
|
1084
|
-
|
|
1085
|
-
if (data && d !== '\r' && d !== '\n') {
|
|
1086
|
-
const cursorPos = this.getCursorPosition()
|
|
1087
|
-
this.openSuggestions(cursorPos, data)
|
|
1088
|
-
} else {
|
|
1084
|
+
if (d === '\r' || d === '\n') {
|
|
1089
1085
|
this.closeSuggestions()
|
|
1086
|
+
return
|
|
1090
1087
|
}
|
|
1088
|
+
// Debounce the suggestion opening to avoid expensive work
|
|
1089
|
+
// (buffer read + getBoundingClientRect + React re-render) on every keystroke
|
|
1090
|
+
this._debouncedOpenSuggestions()
|
|
1091
1091
|
} else {
|
|
1092
1092
|
this.closeSuggestions()
|
|
1093
1093
|
}
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
|
+
_debouncedOpenSuggestions = debounce(function () {
|
|
1097
|
+
const data = this.getCurrentInput()
|
|
1098
|
+
if (!data) {
|
|
1099
|
+
this.closeSuggestions()
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
const cursorPos = this.getCursorPosition()
|
|
1103
|
+
this.openSuggestions(cursorPos, data)
|
|
1104
|
+
}, 80)
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Called by AttachAddonCustom after data is written to the terminal buffer.
|
|
1108
|
+
* This fires after server echo arrives, so getCurrentInput() reflects the
|
|
1109
|
+
* latest state. We trigger a debounced suggestion refresh so the dropdown
|
|
1110
|
+
* updates correctly after backspace, delete, and other edits that rely on
|
|
1111
|
+
* server-side echo to update the buffer.
|
|
1112
|
+
*/
|
|
1113
|
+
onTerminalWrite = () => {
|
|
1114
|
+
if (!this.props.config.showCmdSuggestions) {
|
|
1115
|
+
return
|
|
1116
|
+
}
|
|
1117
|
+
const suggestions = refsStatic.get('terminal-suggestions')
|
|
1118
|
+
if (suggestions?.state?.showSuggestions && !suggestions?.state?.passwordMode) {
|
|
1119
|
+
this._debouncedOpenSuggestions()
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1096
1123
|
loadRenderer = async (term, config) => {
|
|
1097
1124
|
// xterm 6.x: only the built-in DOM renderer and the WebGL addon exist
|
|
1098
1125
|
// (the canvas renderer addon was removed in 6.x). 'dom' = no addon loaded
|