@electerm/electerm-react 3.7.9 → 3.7.18

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.
@@ -2,7 +2,7 @@
2
2
  * AI-powered bookmark generation form
3
3
  */
4
4
  import { useState, useEffect } from 'react'
5
- import { Button, Input, Space, Alert } from 'antd'
5
+ import { Button, Input, Space, Alert, Progress } from 'antd'
6
6
  import message from '../common/message'
7
7
  import {
8
8
  RobotOutlined,
@@ -33,6 +33,16 @@ const EVENT_NAME_HISTORY = 'ai-bookmark-history-update'
33
33
  const { TextArea } = Input
34
34
  const e = window.translate
35
35
 
36
+ function yieldToUI () {
37
+ return new Promise(resolve => {
38
+ if (typeof requestAnimationFrame === 'function') {
39
+ requestAnimationFrame(() => resolve())
40
+ return
41
+ }
42
+ setTimeout(resolve, 0)
43
+ })
44
+ }
45
+
36
46
  export default function AIBookmarkForm (props) {
37
47
  const { onCancel } = props
38
48
  const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
@@ -41,6 +51,7 @@ export default function AIBookmarkForm (props) {
41
51
  const [editMode, setEditMode] = useState(false)
42
52
  const [editorText, setEditorText] = useState('')
43
53
  const [selectedCategory, setSelectedCategory] = useState('default')
54
+ const [confirmProgress, setConfirmProgress] = useState(null)
44
55
 
45
56
  useEffect(() => {
46
57
  setItem(STORAGE_KEY_DESC, description)
@@ -132,7 +143,7 @@ export default function AIBookmarkForm (props) {
132
143
  return arr.map(d => fixBookmarkData(d))
133
144
  }
134
145
 
135
- const createBookmark = async (bm) => {
146
+ const createBookmark = (bm) => {
136
147
  const { store } = window
137
148
  const { addItem } = store
138
149
  const fixedBm = fixBookmarkData(bm)
@@ -163,19 +174,35 @@ export default function AIBookmarkForm (props) {
163
174
 
164
175
  const handleConfirm = async () => {
165
176
  const parsed = getGeneratedData()
166
- if (!parsed.length) {
177
+ if (!parsed.length || confirmProgress) {
167
178
  return
168
179
  }
169
- for (const item of parsed) {
170
- // set defaults like mcpAddBookmark would
171
- await createBookmark(item)
180
+
181
+ setConfirmProgress({ current: 0, total: parsed.length })
182
+
183
+ try {
184
+ for (let i = 0; i < parsed.length; i++) {
185
+ // Yield between synchronous store mutations so large imports stay responsive.
186
+ await yieldToUI()
187
+ createBookmark(parsed[i])
188
+ setConfirmProgress({ current: i + 1, total: parsed.length })
189
+ }
190
+
191
+ setShowConfirm(false)
192
+ setDescription('') // Clear description only on successful creation
193
+ message.success(e('Done'))
194
+ } catch (error) {
195
+ console.error('AI bookmark creation error:', error)
196
+ message.error('Can not create bookmarks from AI response: ' + error.message)
197
+ } finally {
198
+ setConfirmProgress(null)
172
199
  }
173
- setShowConfirm(false)
174
- setDescription('') // Clear description only on successful creation
175
- message.success(e('Done'))
176
200
  }
177
201
 
178
202
  const handleCancelConfirm = () => {
203
+ if (confirmProgress) {
204
+ return
205
+ }
179
206
  setShowConfirm(false)
180
207
  }
181
208
 
@@ -209,6 +236,9 @@ export default function AIBookmarkForm (props) {
209
236
  }
210
237
 
211
238
  const handleToggleEdit = () => {
239
+ if (confirmProgress) {
240
+ return
241
+ }
212
242
  setEditMode(!editMode)
213
243
  }
214
244
 
@@ -219,10 +249,16 @@ export default function AIBookmarkForm (props) {
219
249
  }
220
250
 
221
251
  const handleCopy = () => {
252
+ if (confirmProgress) {
253
+ return
254
+ }
222
255
  copy(editorText)
223
256
  }
224
257
 
225
258
  const handleSaveToFile = async () => {
259
+ if (confirmProgress) {
260
+ return
261
+ }
226
262
  const parsed = getGeneratedData()
227
263
  if (!parsed.length) {
228
264
  return
@@ -261,11 +297,29 @@ export default function AIBookmarkForm (props) {
261
297
  <AICategorySelect
262
298
  bookmarkGroups={window.store.bookmarkGroups}
263
299
  value={selectedCategory}
300
+ disabled={!!confirmProgress}
264
301
  onChange={setSelectedCategory}
265
302
  />
266
303
  )
267
304
  }
268
305
 
306
+ const renderConfirmProgress = () => {
307
+ if (!confirmProgress) {
308
+ return null
309
+ }
310
+ const { current, total } = confirmProgress
311
+ const percent = Math.floor(current * 100 / (total || 1))
312
+ return (
313
+ <div className='pd1y'>
314
+ <Progress
315
+ percent={percent}
316
+ status='active'
317
+ format={() => `${current}/${total}`}
318
+ />
319
+ </div>
320
+ )
321
+ }
322
+
269
323
  const textAreaProps = {
270
324
  value: description,
271
325
  onChange: e => setDescription(e.target.value),
@@ -284,7 +338,7 @@ export default function AIBookmarkForm (props) {
284
338
 
285
339
  function renderQuickConnectBtn () {
286
340
  const parsed = getGeneratedData()
287
- if (!parsed.length || !parsed[0] || parsed.length > 1) {
341
+ if (!parsed.length || !parsed[0] || parsed.length > 1 || confirmProgress) {
288
342
  return null
289
343
  }
290
344
  return (
@@ -295,7 +349,7 @@ export default function AIBookmarkForm (props) {
295
349
  }
296
350
 
297
351
  const modalProps = {
298
- title: e('confirmBookmarkData') || 'Confirm Bookmark Data',
352
+ title: e('bookmarks') + ' ' + e('preview'),
299
353
  open: showConfirm,
300
354
  onCancel: handleCancelConfirm,
301
355
  footer: (
@@ -304,7 +358,12 @@ export default function AIBookmarkForm (props) {
304
358
  <CloseOutlined /> {e('cancel')}
305
359
  </Button>
306
360
  {renderQuickConnectBtn()}
307
- <Button type='primary' onClick={handleConfirm}>
361
+ <Button
362
+ type='primary'
363
+ onClick={handleConfirm}
364
+ loading={!!confirmProgress}
365
+ disabled={!!confirmProgress}
366
+ >
308
367
  <CheckOutlined /> {e('confirm')}
309
368
  </Button>
310
369
  </div>
@@ -315,19 +374,22 @@ export default function AIBookmarkForm (props) {
315
374
  const editBtnProps = {
316
375
  icon: editMode ? <EyeOutlined /> : <EditOutlined />,
317
376
  title: editMode ? e('preview') : e('edit'),
318
- onClick: handleToggleEdit
377
+ onClick: handleToggleEdit,
378
+ disabled: !!confirmProgress
319
379
  }
320
380
 
321
381
  const copyBtnProps = {
322
382
  icon: <CopyOutlined />,
323
383
  title: e('copy'),
324
- onClick: handleCopy
384
+ onClick: handleCopy,
385
+ disabled: !!confirmProgress
325
386
  }
326
387
 
327
388
  const downloadBtnProps = {
328
389
  icon: <DownloadOutlined />,
329
390
  title: e('download'),
330
- onClick: handleSaveToFile
391
+ onClick: handleSaveToFile,
392
+ disabled: !!confirmProgress
331
393
  }
332
394
 
333
395
  const cancelProps = {
@@ -378,6 +440,7 @@ export default function AIBookmarkForm (props) {
378
440
  <Button {...downloadBtnProps} />
379
441
  </Space.Compact>
380
442
  </div>
443
+ {renderConfirmProgress()}
381
444
  <div className='pd1y'>
382
445
  {renderCategorySelect()}
383
446
  </div>
@@ -6,9 +6,9 @@ const bookmarkSchema = {
6
6
  username: 'string (required) - SSH username',
7
7
  password: 'string - password for authentication',
8
8
  privateKey: 'string - private key content or path for key-based auth',
9
- passphrase: 'string - passphrase for private key/cetificate',
9
+ passphrase: 'string - passphrase for private key/certificate',
10
10
  certificate: 'string - certificate content',
11
- authType: 'string - auth type (password|privateKey|profiles)',
11
+ authType: 'string - auth type (password|privateKey|profiles), when have profile, should be profiles',
12
12
  profile: 'string - profile id to reuse saved auth',
13
13
  title: 'string - bookmark title',
14
14
  description: 'string - bookmark description',
@@ -6,7 +6,8 @@ const e = window.translate
6
6
  export default function AICategorySelect ({
7
7
  bookmarkGroups = [],
8
8
  value,
9
- onChange
9
+ onChange,
10
+ disabled = false
10
11
  }) {
11
12
  const tree = formatBookmarkGroups(bookmarkGroups)
12
13
 
@@ -24,6 +25,7 @@ export default function AICategorySelect ({
24
25
  treeData={tree}
25
26
  treeDefaultExpandAll
26
27
  showSearch
28
+ disabled={disabled}
27
29
  onChange={handleChange}
28
30
  style={{ minWidth: 200 }}
29
31
  />
@@ -57,6 +57,7 @@ export default class BookmarkIndex2 extends PureComponent {
57
57
 
58
58
  componentWillUnmount () {
59
59
  clearTimeout(this.timer)
60
+ clearTimeout(this.timer1)
60
61
  }
61
62
 
62
63
  getInitAiModeState () {
@@ -64,7 +65,10 @@ export default class BookmarkIndex2 extends PureComponent {
64
65
  if (v !== true) {
65
66
  return false
66
67
  }
67
- delete window.et.openBookmarkWithAIMode
68
+ this.timer1 = setTimeout(() => {
69
+ delete window.et.openBookmarkWithAIMode
70
+ }, 1000)
71
+
68
72
  return true
69
73
  }
70
74
 
@@ -143,11 +143,9 @@ export function getShellIntegrationCommand (shellType = 'bash') {
143
143
  return wrapSilent(cmd, shellType)
144
144
  }
145
145
  export async function detectRemoteShell (pid) {
146
- // 1. We try the version variables first.
147
- // 2. We try your verified fish check: fish --version ...
148
- // 3. We use ps -p $$ to check the process name (highly reliable in Linux/Docker).
149
- // This syntax is safe for Bash, Zsh, and Fish.
150
- const cmd = 'fish --version 2>/dev/null | grep -q fish && echo fish || { env | grep -q ZSH_VERSION && echo zsh || { env | grep -q BASH_VERSION && echo bash || { ps -p $$ -o comm= 2>/dev/null || echo sh; }; }; }'
146
+ // SSH exec runs under the account shell, so prefer the configured shell path
147
+ // instead of probing for any shell binary installed on the host.
148
+ const cmd = 'printf "%s\n" "$SHELL"'
151
149
 
152
150
  const r = await runCmd(pid, cmd)
153
151
  .catch((err) => {
@@ -157,8 +155,9 @@ export async function detectRemoteShell (pid) {
157
155
 
158
156
  const shell = r.trim().toLowerCase()
159
157
 
160
- if (shell.includes('fish')) return 'fish'
161
- if (shell.includes('zsh')) return 'zsh'
162
- if (shell.includes('bash')) return 'bash'
163
- return 'sh' // Fallback for sh/ash/dash
158
+ if (!shell) {
159
+ return 'sh'
160
+ }
161
+
162
+ return detectShellType(shell)
164
163
  }
@@ -953,6 +953,7 @@ class Term extends Component {
953
953
  this.term = term
954
954
  term.onSelectionChange(this.onSelectionChange)
955
955
  term.attachCustomKeyEventHandler(this.handleKeyboardEvent.bind(this))
956
+ this.fitAddon.fit()
956
957
  await this.remoteInit(term)
957
958
  }
958
959
 
@@ -1,9 +1,29 @@
1
+ import { memo } from 'react'
2
+
1
3
  import {
2
4
  CaretDownOutlined,
3
5
  CaretRightOutlined
4
6
  } from '@ant-design/icons'
5
7
 
6
- export default function TreeExpander (props) {
8
+ function hasChildren (group) {
9
+ return Boolean(
10
+ group?.bookmarkIds?.length ||
11
+ group?.bookmarkGroupIds?.length
12
+ )
13
+ }
14
+
15
+ function isOpen (props) {
16
+ return Boolean(props.keyword) || props.expandedKeys.includes(props.group.id)
17
+ }
18
+
19
+ function areEqual (prevProps, nextProps) {
20
+ return prevProps.group?.id === nextProps.group?.id &&
21
+ hasChildren(prevProps.group) === hasChildren(nextProps.group) &&
22
+ Boolean(prevProps.keyword) === Boolean(nextProps.keyword) &&
23
+ isOpen(prevProps) === isOpen(nextProps)
24
+ }
25
+
26
+ function TreeExpander (props) {
7
27
  function onExpand (e) {
8
28
  e.stopPropagation()
9
29
  props.onExpand(group)
@@ -35,3 +55,5 @@ export default function TreeExpander (props) {
35
55
  </div>
36
56
  )
37
57
  }
58
+
59
+ export default memo(TreeExpander, areEqual)
@@ -2,6 +2,8 @@
2
2
  * tree list for bookmarks
3
3
  */
4
4
 
5
+ import { memo, useState } from 'react'
6
+
5
7
  import {
6
8
  CloseOutlined,
7
9
  CopyOutlined,
@@ -24,7 +26,31 @@ import uid from '../../common/uid'
24
26
 
25
27
  const e = window.translate
26
28
 
27
- export default function TreeListItem (props) {
29
+ function getItemLabel (item, isGroup) {
30
+ return isGroup
31
+ ? item?.title || ''
32
+ : createName(item)
33
+ }
34
+
35
+ function areEqual (prevProps, nextProps) {
36
+ const prevSelected = prevProps.selectedItemId === prevProps.item.id
37
+ const nextSelected = nextProps.selectedItemId === nextProps.item.id
38
+
39
+ return prevProps.isGroup === nextProps.isGroup &&
40
+ prevProps.parentId === nextProps.parentId &&
41
+ prevProps.staticList === nextProps.staticList &&
42
+ prevProps.keyword === nextProps.keyword &&
43
+ prevSelected === nextSelected &&
44
+ prevProps.item.id === nextProps.item.id &&
45
+ prevProps.item.level === nextProps.item.level &&
46
+ prevProps.item.color === nextProps.item.color &&
47
+ prevProps.item.description === nextProps.item.description &&
48
+ getItemLabel(prevProps.item, prevProps.isGroup) === getItemLabel(nextProps.item, nextProps.isGroup)
49
+ }
50
+
51
+ function TreeListItem (props) {
52
+ const [hovered, setHovered] = useState(false)
53
+
28
54
  const handleDel = (e) => {
29
55
  props.del(props.item, e)
30
56
  }
@@ -225,6 +251,8 @@ export default function TreeListItem (props) {
225
251
  'data-item-id': item.id,
226
252
  'data-parent-id': props.parentId,
227
253
  'data-is-group': isGroup ? 'true' : 'false',
254
+ onMouseEnter: () => setHovered(true),
255
+ onMouseLeave: () => setHovered(false),
228
256
  onDragOver,
229
257
  onDragStart,
230
258
  onDragEnter,
@@ -250,18 +278,20 @@ export default function TreeListItem (props) {
250
278
  {colorTag}{tag}{titleHighlight}
251
279
  </div>
252
280
  {
253
- isGroup
281
+ hovered && isGroup
254
282
  ? renderGroupBtns()
255
283
  : null
256
284
  }
257
285
  {
258
- !isGroup
286
+ hovered && !isGroup
259
287
  ? renderDuplicateBtn()
260
288
  : null
261
289
  }
262
- {renderOperationBtn()}
263
- {renderDelBtn()}
264
- {renderEditBtn()}
290
+ {hovered ? renderOperationBtn() : null}
291
+ {hovered ? renderDelBtn() : null}
292
+ {hovered ? renderEditBtn() : null}
265
293
  </div>
266
294
  )
267
295
  }
296
+
297
+ export default memo(TreeListItem, areEqual)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.7.9",
3
+ "version": "3.7.18",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",