@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.
@@ -143,11 +143,14 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
143
143
  return { label, title }
144
144
  }
145
145
 
146
- function renderApiUrlLabel () {
146
+ function renderApiKeyLabel () {
147
147
  if (baseURLAI === 'https://api.atlascloud.ai/v1') {
148
- return <span>API URL (<Link to='https://atlascloud.ai'>AtlasCloud</Link>)</span>
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
- return 'API URL'
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={renderApiUrlLabel()} required>
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='API Key'
232
+ label={renderApiKeyLabel()}
230
233
  name='apiKeyAI'
231
234
  >
232
235
  <Password placeholder='Enter your API key' />
@@ -92,7 +92,7 @@ export default () => {
92
92
  },
93
93
  {
94
94
  name: 'terminal_paste',
95
- shortcut: 'ctrl+v,ctrl+shift+v',
95
+ shortcut: 'ctrl+shift+v',
96
96
  shortcutMac: 'meta+v',
97
97
  readonly: true
98
98
  },
@@ -224,7 +224,12 @@ export default class AttachAddonCustom {
224
224
 
225
225
  if (typeof data === 'string') {
226
226
  term?.parent?.notifyOnData()
227
- return term.write(data)
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
- id: uid(),
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: uid(),
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
- } = this.props.suggestions || {}
308
+ } = suggestions || {}
297
309
  const res = []
298
- this.state.aiSuggestions
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: uid(),
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
- return this.state.reverse ? res.reverse() : res
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
- const data = this.getCurrentInput()
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.15.126",
3
+ "version": "3.15.128",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",