@electerm/electerm-react 3.7.9 → 3.7.16
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/bookmark-form/ai-bookmark-form.jsx +78 -15
- package/client/components/bookmark-form/bookmark-schema.js +2 -2
- package/client/components/bookmark-form/common/ai-category-select.jsx +3 -1
- package/client/components/bookmark-form/index.jsx +5 -1
- package/client/components/terminal/shell.js +8 -9
- package/client/components/terminal/terminal.jsx +1 -0
- package/client/components/tree-list/tree-expander.jsx +23 -1
- package/client/components/tree-list/tree-list-item.jsx +36 -6
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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('
|
|
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
|
|
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/
|
|
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
|
-
|
|
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
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
if (!shell) {
|
|
159
|
+
return 'sh'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return detectShellType(shell)
|
|
164
163
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|