@electerm/electerm-react 2.10.26 → 2.11.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 (35) hide show
  1. package/client/common/default-setting.js +1 -0
  2. package/client/common/has-active-input.js +1 -1
  3. package/client/common/parse-quick-connect.js +438 -0
  4. package/client/components/ai/ai-chat-history-item.jsx +1 -1
  5. package/client/components/ai/ai-config.jsx +1 -1
  6. package/client/components/bookmark-form/ai-bookmark-form.jsx +40 -1
  7. package/client/components/bookmark-form/bookmark-form.styl +21 -0
  8. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +141 -0
  9. package/client/components/bookmark-form/tree-select.jsx +72 -23
  10. package/client/components/common/input-context-menu.jsx +13 -5
  11. package/client/components/main/main.jsx +3 -0
  12. package/client/components/rdp/rdp-session.jsx +4 -8
  13. package/client/components/rdp/rdp.styl +15 -0
  14. package/client/components/setting-panel/bookmark-tree-list.jsx +1 -0
  15. package/client/components/setting-panel/list.styl +10 -4
  16. package/client/components/setting-panel/setting-terminal.jsx +3 -2
  17. package/client/components/sftp/list-table-ui.jsx +29 -2
  18. package/client/components/sftp/paged-list.jsx +3 -8
  19. package/client/components/sidebar/history-item.jsx +13 -1
  20. package/client/components/sidebar/index.jsx +13 -10
  21. package/client/components/spice/spice.styl +7 -0
  22. package/client/components/tabs/add-btn-menu.jsx +2 -0
  23. package/client/components/tabs/no-session.jsx +25 -9
  24. package/client/components/tabs/no-session.styl +21 -0
  25. package/client/components/tabs/quick-connect.jsx +130 -0
  26. package/client/components/tabs/tabs.styl +1 -19
  27. package/client/components/terminal/highlight-addon.js +11 -0
  28. package/client/components/terminal/terminal-interactive.jsx +1 -0
  29. package/client/components/terminal/terminal.jsx +16 -1
  30. package/client/components/terminal/trzsz-client.js +6 -0
  31. package/client/components/terminal/xterm-loader.js +11 -0
  32. package/client/components/terminal-info/run-cmd.jsx +2 -1
  33. package/client/store/load-data.js +4 -0
  34. package/client/store/sync.js +1 -0
  35. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Bookmark from history modal - used to create bookmark from history item
3
+ */
4
+
5
+ import React from 'react'
6
+ import { Button, message } from 'antd'
7
+ import { PlusOutlined } from '@ant-design/icons'
8
+ import Modal from '../common/modal'
9
+ import { refsStatic } from '../common/ref'
10
+ import AICategorySelect from './common/ai-category-select.jsx'
11
+ import generate from '../../common/uid'
12
+ import copy from 'json-deep-copy'
13
+
14
+ const e = window.translate
15
+
16
+ export default class BookmarkFromHistoryModal extends React.PureComponent {
17
+ state = {
18
+ visible: false,
19
+ tab: null,
20
+ selectedCategory: 'default'
21
+ }
22
+
23
+ componentDidMount () {
24
+ refsStatic.add('bookmark-from-history-modal', this)
25
+ }
26
+
27
+ show (tab) {
28
+ this.setState({
29
+ visible: true,
30
+ tab: copy(tab),
31
+ selectedCategory: 'default'
32
+ })
33
+ }
34
+
35
+ handleClose = () => {
36
+ this.setState({
37
+ visible: false,
38
+ tab: null,
39
+ selectedCategory: 'default'
40
+ })
41
+ }
42
+
43
+ buildBookmark = () => {
44
+ const { tab } = this.state
45
+ if (!tab) return null
46
+
47
+ const r = {
48
+ ...tab,
49
+ id: generate()
50
+ }
51
+ console.log(r)
52
+ delete r.parentId
53
+ delete r.category
54
+ return r
55
+ }
56
+
57
+ handleConfirm = () => {
58
+ const { tab, selectedCategory } = this.state
59
+ if (!tab) {
60
+ return
61
+ }
62
+
63
+ const { store } = window
64
+ const { addItem } = store
65
+
66
+ // Create bookmark from tab data
67
+ const bookmark = this.buildBookmark()
68
+
69
+ // Add bookmark
70
+ addItem(bookmark, 'bookmarks')
71
+
72
+ // Ensure the bookmark id is registered in its group
73
+ try {
74
+ const groupId = selectedCategory || 'default'
75
+ const group = window.store.bookmarkGroups.find(g => g.id === groupId)
76
+ if (group) {
77
+ group.bookmarkIds = [
78
+ ...new Set([...(group.bookmarkIds || []), bookmark.id])
79
+ ]
80
+ bookmark.color = group.color
81
+ }
82
+ } catch (err) {
83
+ console.error('Failed to update bookmark group:', err)
84
+ }
85
+
86
+ message.success(e('Done'))
87
+ this.handleClose()
88
+ }
89
+
90
+ render () {
91
+ const { visible, selectedCategory } = this.state
92
+
93
+ if (!visible) {
94
+ return null
95
+ }
96
+
97
+ const bookmark = this.buildBookmark()
98
+ const bookmarkJson = JSON.stringify(bookmark, null, 2)
99
+
100
+ const modalProps = {
101
+ open: visible,
102
+ title: (
103
+ <span>
104
+ <PlusOutlined className='mg1r' />
105
+ {e('bookmarks')}
106
+ </span>
107
+ ),
108
+ width: 600,
109
+ onCancel: this.handleClose,
110
+ footer: (
111
+ <div className='custom-modal-footer-buttons'>
112
+ <Button onClick={this.handleClose}>
113
+ {e('cancel')}
114
+ </Button>
115
+ <Button type='primary' onClick={this.handleConfirm}>
116
+ {e('confirm')}
117
+ </Button>
118
+ </div>
119
+ )
120
+ }
121
+
122
+ return (
123
+ <Modal {...modalProps}>
124
+ <div className='bookmark-from-history-modal pd2'>
125
+ <div className='pd1b'>
126
+ <AICategorySelect
127
+ bookmarkGroups={window.store.bookmarkGroups}
128
+ value={selectedCategory}
129
+ onChange={(val) => this.setState({ selectedCategory: val })}
130
+ />
131
+ </div>
132
+ <div className='pd1b'>
133
+ <pre className='bookmark-json-preview'>
134
+ {bookmarkJson}
135
+ </pre>
136
+ </div>
137
+ </div>
138
+ </Modal>
139
+ )
140
+ }
141
+ }
@@ -1,15 +1,18 @@
1
1
  import {
2
2
  Tree,
3
3
  Button,
4
- Space
4
+ Space,
5
+ Input
5
6
  } from 'antd'
7
+ import { useState, useMemo } from 'react'
6
8
  import { defaultBookmarkGroupId, settingMap } from '../../common/constants'
7
9
  import deepCopy from 'json-deep-copy'
8
- import { createTitleWithTag } from '../../common/create-title'
10
+ import createTitle, { createTitleWithTag } from '../../common/create-title'
9
11
 
10
12
  const e = window.translate
11
13
 
12
- function buildData (bookmarks, bookmarkGroups) {
14
+ function buildData (bookmarks, bookmarkGroups, searchText = '') {
15
+ const searchLower = searchText.toLowerCase()
13
16
  const cats = bookmarkGroups
14
17
  const tree = bookmarks
15
18
  .reduce((p, k) => {
@@ -25,6 +28,14 @@ function buildData (bookmarks, bookmarkGroups) {
25
28
  [k.id]: k
26
29
  }
27
30
  }, {})
31
+
32
+ // Helper to check if a node matches the search
33
+ function matchesSearch (text) {
34
+ if (!searchText) return true
35
+ const str = String(text || '')
36
+ return str.toLowerCase().includes(searchLower)
37
+ }
38
+
28
39
  function buildSubCats (id) {
29
40
  const x = btree[id]
30
41
  if (!x) {
@@ -42,6 +53,14 @@ function buildData (bookmarks, bookmarkGroups) {
42
53
  if (y.children && !y.children.length) {
43
54
  delete y.children
44
55
  }
56
+ // Filter: include node if it matches or has matching children
57
+ if (searchText) {
58
+ const titleMatches = matchesSearch(x.title || '')
59
+ const hasMatchingChildren = y.children && y.children.length > 0
60
+ if (!titleMatches && !hasMatchingChildren) {
61
+ return ''
62
+ }
63
+ }
45
64
  return y
46
65
  }
47
66
  function buildLeaf (id) {
@@ -49,6 +68,11 @@ function buildData (bookmarks, bookmarkGroups) {
49
68
  if (!x) {
50
69
  return ''
51
70
  }
71
+ const titleText = createTitle(x)
72
+ // Filter: only include leaf if it matches search
73
+ if (searchText && !matchesSearch(titleText)) {
74
+ return ''
75
+ }
52
76
  return {
53
77
  value: x.id,
54
78
  key: x.id,
@@ -57,14 +81,26 @@ function buildData (bookmarks, bookmarkGroups) {
57
81
  }
58
82
  const level1 = cats.filter(d => d.level !== 2)
59
83
  .map(d => {
84
+ const children = [
85
+ ...(d.bookmarkGroupIds || []).map(buildSubCats),
86
+ ...(d.bookmarkIds || []).map(buildLeaf)
87
+ ].filter(d => d)
88
+ // Filter: include group if it matches or has matching children
89
+ if (searchText) {
90
+ const titleMatches = matchesSearch(d.title || '')
91
+ const hasMatchingChildren = children.length > 0
92
+ if (!titleMatches && !hasMatchingChildren) {
93
+ return null
94
+ }
95
+ }
60
96
  const r = {
61
97
  title: d.title,
62
98
  value: d.id,
63
99
  key: d.id,
64
- children: [
65
- ...(d.bookmarkGroupIds || []).map(buildSubCats),
66
- ...(d.bookmarkIds || []).map(buildLeaf)
67
- ].filter(d => d)
100
+ children
101
+ }
102
+ if (r.children && !r.children.length) {
103
+ delete r.children
68
104
  }
69
105
  return r
70
106
  }).filter(d => d)
@@ -72,18 +108,13 @@ function buildData (bookmarks, bookmarkGroups) {
72
108
  }
73
109
 
74
110
  export default function BookmarkTreeSelect (props) {
75
- const { expandedKeys: propExpandedKeys, checkedKeys: propCheckedKeys, bookmarks, bookmarkGroups, type = 'delete', onCheck: propOnCheck, onExpand: propOnExpand } = props
76
-
77
- const expandedKeys = propExpandedKeys !== undefined ? propExpandedKeys : window.store.expandedKeys
78
- const checkedKeys = propCheckedKeys !== undefined ? propCheckedKeys : window.store.checkedKeys
111
+ const { bookmarks, bookmarkGroups, type = 'delete', expandedKeys: propExpandedKeys, checkedKeys: propCheckedKeys } = props
79
112
 
80
- const onExpand = propOnExpand || ((expandedKeys) => {
81
- window.store.expandedKeys = deepCopy(expandedKeys)
82
- })
113
+ const [expandedKeys, setExpandedKeys] = useState(() => deepCopy(propExpandedKeys || []))
114
+ const [checkedKeys, setCheckedKeys] = useState(() => deepCopy(propCheckedKeys || []))
115
+ const [searchText, setSearchText] = useState('')
83
116
 
84
- const onCheck = propOnCheck || ((checkedKeys) => {
85
- window.store.checkedKeys = deepCopy(checkedKeys)
86
- })
117
+ const onCheck = setCheckedKeys
87
118
 
88
119
  const handleOperation = () => {
89
120
  const { store } = window
@@ -97,7 +128,7 @@ export default function BookmarkTreeSelect (props) {
97
128
  props.onClose()
98
129
  }
99
130
  }
100
- store.checkedKeys = []
131
+ setCheckedKeys([])
101
132
  }
102
133
 
103
134
  const handleCancel = () => {
@@ -107,7 +138,14 @@ export default function BookmarkTreeSelect (props) {
107
138
  } else {
108
139
  store.bookmarkSelectMode = false
109
140
  }
110
- store.checkedKeys = []
141
+ setCheckedKeys([])
142
+ }
143
+
144
+ const treeData = useMemo(() => buildData(bookmarks, bookmarkGroups, searchText), [bookmarks, bookmarkGroups, searchText])
145
+
146
+ // Auto expand parent nodes when searching
147
+ const handleExpand = (keys) => {
148
+ setExpandedKeys(keys)
111
149
  }
112
150
 
113
151
  const rProps = {
@@ -116,13 +154,22 @@ export default function BookmarkTreeSelect (props) {
116
154
  onCheck,
117
155
  expandedKeys,
118
156
  checkedKeys,
119
- onExpand,
120
- treeData: buildData(bookmarks, bookmarkGroups)
157
+ onExpand: handleExpand,
158
+ treeData
121
159
  }
122
160
  const len = checkedKeys.length
123
161
  return (
124
- <div>
125
- <div className='pd2'>
162
+ <div className='tree-select-wrapper pd2'>
163
+ <div className='tree-select-header'>
164
+ <Space.Compact className='mg2b'>
165
+ <Input.Search
166
+ placeholder={e('search') || 'Search...'}
167
+ value={searchText}
168
+ onChange={(e) => setSearchText(e.target.value)}
169
+ allowClear
170
+ style={{ flex: 1 }}
171
+ />
172
+ </Space.Compact>
126
173
  <Space.Compact className='mg2b'>
127
174
  <Button
128
175
  type='primary'
@@ -137,6 +184,8 @@ export default function BookmarkTreeSelect (props) {
137
184
  {e('cancel')}
138
185
  </Button>
139
186
  </Space.Compact>
187
+ </div>
188
+ <div className='tree-select-content'>
140
189
  <Tree {...rProps} />
141
190
  </div>
142
191
  </div>
@@ -63,7 +63,6 @@ function handleCopy (element) {
63
63
  */
64
64
  async function handlePaste (element) {
65
65
  if (!isInputElement(element) || element.readOnly || element.disabled) return
66
-
67
66
  element.focus()
68
67
  const clipboardText = await readClipboardAsync()
69
68
 
@@ -89,7 +88,7 @@ function insertTextAtCursor (element, text) {
89
88
  triggerInputEvents(element)
90
89
  }
91
90
  } catch (error) {
92
- console.log('execCommand insertText failed:', error)
91
+ console.error('execCommand insertText failed:', error)
93
92
  }
94
93
  }
95
94
  }
@@ -135,7 +134,7 @@ function handleCut (element) {
135
134
  triggerInputEvents(element)
136
135
  }
137
136
  } catch (error) {
138
- console.log('execCommand cut failed:', error)
137
+ console.error('execCommand cut failed:', error)
139
138
  }
140
139
  }
141
140
  }
@@ -197,12 +196,15 @@ const InputContextMenu = () => {
197
196
  const [targetElement, setTargetElement] = useState(null)
198
197
  const menuRef = useRef(null)
199
198
 
200
- const handleMenuClick = async ({ key }) => {
199
+ const handleMenuClick = async ({ key, domEvent }) => {
200
+ domEvent.preventDefault()
201
+ domEvent.stopPropagation()
201
202
  // Keep reference to current element before closing menu
202
203
  const currentElement = targetElement
203
204
  // Close menu first
204
205
  setVisible(false)
205
206
  setTargetElement(null)
207
+
206
208
  // Execute action with preserved element reference
207
209
  if (currentElement) {
208
210
  setTimeout(async () => {
@@ -246,7 +248,12 @@ const InputContextMenu = () => {
246
248
  }
247
249
 
248
250
  const handleClick = (event) => {
249
- if (visible && menuRef.current && !menuRef.current.contains(event.target)) {
251
+ const isInDropdown = event.target.closest('.ant-dropdown')
252
+ if (
253
+ visible &&
254
+ menuRef.current &&
255
+ !isInDropdown
256
+ ) {
250
257
  setVisible(false)
251
258
  setTargetElement(null)
252
259
  }
@@ -284,6 +291,7 @@ const InputContextMenu = () => {
284
291
  }}
285
292
  open={visible}
286
293
  placement='bottomLeft'
294
+ className='input-context-menu'
287
295
  >
288
296
  <div style={{ width: 1, height: 1 }} />
289
297
  </Dropdown>
@@ -32,6 +32,7 @@ import Opacity from '../common/opacity'
32
32
  import MoveItemModal from '../tree-list/move-item-modal'
33
33
  import InputContextMenu from '../common/input-context-menu'
34
34
  import WorkspaceSaveModal from '../tabs/workspace-save-modal'
35
+ import BookmarkFromHistoryModal from '../bookmark-form/bookmark-from-history-modal'
35
36
  import { pick } from 'lodash-es'
36
37
  import deepCopy from 'json-deep-copy'
37
38
  import './wrapper.styl'
@@ -47,6 +48,7 @@ export default auto(function Index (props) {
47
48
  ipcOnEvent('open-about', store.openAbout)
48
49
  ipcOnEvent('new-ssh', store.onNewSsh)
49
50
  ipcOnEvent('add-tab-from-command-line', store.addTabFromCommandLine)
51
+ ipcOnEvent('open-tab', store.addTab)
50
52
  ipcOnEvent('openSettings', store.openSetting)
51
53
  ipcOnEvent('selectall', store.selectall)
52
54
  ipcOnEvent('focused', store.focus)
@@ -293,6 +295,7 @@ export default auto(function Index (props) {
293
295
  <TerminalCmdSuggestions {...cmdSuggestionsProps} />
294
296
  <TransferQueue />
295
297
  <WorkspaceSaveModal store={store} />
298
+ <BookmarkFromHistoryModal />
296
299
  <NotificationContainer />
297
300
  </div>
298
301
  </ConfigProvider>
@@ -20,6 +20,7 @@ import scanCode from './code-scan'
20
20
  import resolutions from './resolutions'
21
21
  import { readClipboardAsync } from '../../common/clipboard'
22
22
  import RemoteFloatControl from '../common/remote-float-control'
23
+ import './rdp.styl'
23
24
 
24
25
  const { Option } = Select
25
26
 
@@ -538,7 +539,7 @@ export default class RdpSession extends PureComponent {
538
539
  }
539
540
  return (
540
541
  <div
541
- className='pd1 fix session-v-info'
542
+ className='pd1 fix session-v-info block'
542
543
  >
543
544
  <div className='fleft'>
544
545
  <ReloadOutlined
@@ -613,14 +614,9 @@ export default class RdpSession extends PureComponent {
613
614
  tabIndex: 0
614
615
  }
615
616
  if (scaleViewport) {
616
- Object.assign(canvasProps, {
617
- style: {
618
- width: '100%',
619
- objectFit: 'contain'
620
- }
621
- })
617
+ canvasProps.className = 'scale-viewport'
622
618
  }
623
- const cls = 'rdp-session-wrap session-v-wrap'
619
+ const cls = `rdp-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
624
620
  const controlProps = this.getControlProps()
625
621
  return (
626
622
  <Spin spinning={loading}>
@@ -0,0 +1,15 @@
1
+ .rdp-session-wrap
2
+ display: flex
3
+ flex-direction: column
4
+ justify-content: center
5
+ align-items: center
6
+ .session-v-info
7
+ position: absolute
8
+ top: 0
9
+ left: 0
10
+ width: 100%
11
+ &.scale-viewport
12
+ canvas
13
+ width: 100% !important
14
+ object-fit: contain
15
+
@@ -6,6 +6,7 @@ export default function BookmarkTreeList (props) {
6
6
  ? (
7
7
  <BookmarkTreeSelect
8
8
  {...props}
9
+ type='delete'
9
10
  />
10
11
  )
11
12
  : (
@@ -6,6 +6,7 @@
6
6
  .list-item-edit
7
7
  .list-item-apply
8
8
  .list-item-remove
9
+ .list-item-bookmark
9
10
  display none
10
11
  width 24px
11
12
  line-height 35px
@@ -13,6 +14,7 @@
13
14
  position absolute
14
15
  right 0
15
16
  top 0
17
+
16
18
  .list-item-title
17
19
  flex-grow: 1
18
20
  .item-list-unit
@@ -32,8 +34,12 @@
32
34
  .list-item-apply
33
35
  .list-item-edit
34
36
  .list-item-remove
37
+ .list-item-bookmark
35
38
  display block
36
- .item-list
37
- .list-item-edit
38
- .list-item-apply
39
- right 20px
39
+ // .item-list
40
+ // .list-item-edit
41
+ // .list-item-apply
42
+ // right 20px
43
+ .item-list-unit
44
+ .list-item-bookmark
45
+ right 18px
@@ -117,9 +117,9 @@ export default class SettingTerminal extends Component {
117
117
  return this.saveConfig(data)
118
118
  }
119
119
 
120
- renderToggle = (name, cls = 'pd2b') => {
120
+ renderToggle = (name, cls = 'pd2b', label) => {
121
121
  const checked = !!this.props.config[name]
122
- const txt = e(name)
122
+ const txt = label || e(name)
123
123
  return (
124
124
  <div className={cls} key={'rt' + name}>
125
125
  <Switch
@@ -580,6 +580,7 @@ export default class SettingTerminal extends Component {
580
580
  this.renderToggle('saveTerminalLogToFile')
581
581
  }
582
582
  {this.renderToggle('addTimeStampToTermLog')}
583
+ {this.renderToggle('enableSixel', 'pd2b', 'SIXEL')}
583
584
  {
584
585
  [
585
586
  'cursorBlink',
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { Component } from 'react'
6
- import { Dropdown } from 'antd'
6
+ import { Dropdown, Pagination } from 'antd'
7
7
  import classnames from 'classnames'
8
8
  import FileSection from './file-item'
9
9
  import PagedList from './paged-list'
@@ -37,7 +37,8 @@ export default class FileListTable extends Component {
37
37
  })
38
38
  return {
39
39
  pageSize: 100,
40
- properties
40
+ properties,
41
+ page: 1
41
42
  }
42
43
  }
43
44
 
@@ -181,6 +182,29 @@ export default class FileListTable extends Component {
181
182
  return len > pageSize
182
183
  }
183
184
 
185
+ onPageChange = (page) => {
186
+ this.setState({ page })
187
+ }
188
+
189
+ renderPager () {
190
+ const { page, pageSize } = this.state
191
+ const { fileList } = this.props
192
+ const props = {
193
+ current: page,
194
+ pageSize,
195
+ total: fileList.length,
196
+ showLessItems: true,
197
+ showSizeChanger: false,
198
+ simple: false,
199
+ onChange: this.onPageChange
200
+ }
201
+ return (
202
+ <div className='pd1b pager-wrap'>
203
+ <Pagination {...props} />
204
+ </div>
205
+ )
206
+ }
207
+
184
208
  // reset
185
209
  resetWidth = () => {
186
210
  this.setState(this.initFromProps())
@@ -315,9 +339,12 @@ export default class FileListTable extends Component {
315
339
  list={fileList}
316
340
  renderItem={this.renderItem}
317
341
  hasPager={hasPager}
342
+ page={this.state.page}
343
+ pageSize={this.state.pageSize}
318
344
  />
319
345
  </div>
320
346
  </Dropdown>
347
+ {hasPager && this.renderPager()}
321
348
  </div>
322
349
  )
323
350
  }
@@ -18,9 +18,8 @@ export default class ScrollFiles extends Component {
18
18
  }
19
19
 
20
20
  renderList () {
21
- const {
22
- page, pageSize
23
- } = this.state
21
+ const page = this.props.page ?? this.state.page
22
+ const pageSize = this.props.pageSize ?? this.state.pageSize
24
23
  const start = (page - 1) * pageSize
25
24
  const end = start + pageSize
26
25
  const {
@@ -52,10 +51,6 @@ export default class ScrollFiles extends Component {
52
51
  }
53
52
 
54
53
  render () {
55
- const arr = this.renderList()
56
- if (this.props.hasPager) {
57
- arr.push(this.renderPager())
58
- }
59
- return arr
54
+ return this.renderList()
60
55
  }
61
56
  }
@@ -1,6 +1,7 @@
1
1
  import React, { useCallback, useEffect, useRef } from 'react'
2
2
  import createTitle, { createTitleWithTag } from '../../common/create-title'
3
- import { DeleteOutlined } from '@ant-design/icons'
3
+ import { DeleteOutlined, BookFilled } from '@ant-design/icons'
4
+ import { refsStatic } from '../common/ref'
4
5
 
5
6
  export default function HistoryItem (props) {
6
7
  const { store } = window
@@ -31,6 +32,12 @@ export default function HistoryItem (props) {
31
32
  e.stopPropagation()
32
33
  store.history.splice(index, 1)
33
34
  }
35
+
36
+ function handleBookmark (e) {
37
+ e.stopPropagation()
38
+ refsStatic.get('bookmark-from-history-modal')?.show(item.tab)
39
+ }
40
+
34
41
  const title = createTitleWithTag(item.tab)
35
42
  const tt = createTitle(item.tab)
36
43
  return (
@@ -42,6 +49,11 @@ export default function HistoryItem (props) {
42
49
  <div className='elli pd1y pd2x'>
43
50
  {title}
44
51
  </div>
52
+ <BookFilled
53
+ className='list-item-bookmark'
54
+ title={window.translate('bookmark')}
55
+ onClick={handleBookmark}
56
+ />
45
57
  <DeleteOutlined
46
58
  className='list-item-edit'
47
59
  onClick={handleDelete}
@@ -8,12 +8,13 @@ import {
8
8
  UpCircleOutlined,
9
9
  BarsOutlined,
10
10
  AppstoreOutlined,
11
- RobotOutlined
11
+ ThunderboltOutlined
12
12
  } from '@ant-design/icons'
13
- import { Tooltip } from 'antd'
13
+ import { Tooltip, Popover } from 'antd'
14
14
  import SideBarPanel from './sidebar-panel'
15
15
  import TransferList from './transfer-list'
16
16
  import MenuBtn from '../sys-menu/menu-btn'
17
+ import QuickConnect from '../tabs/quick-connect'
17
18
  import {
18
19
  sidebarWidth,
19
20
  settingMap,
@@ -87,7 +88,6 @@ export default function Sidebar (props) {
87
88
 
88
89
  const {
89
90
  onNewSsh,
90
- onNewSshAI,
91
91
  openSetting,
92
92
  openAbout,
93
93
  openSettingSync,
@@ -144,14 +144,17 @@ export default function Sidebar (props) {
144
144
  onClick={onNewSsh}
145
145
  />
146
146
  </SideIcon>
147
- <SideIcon
148
- title={e('createBookmarkByAI')}
147
+ <Popover
148
+ content={<QuickConnect inputOnly />}
149
+ trigger='click'
150
+ placement='right'
149
151
  >
150
- <RobotOutlined
151
- className='font20 iblock control-icon'
152
- onClick={onNewSshAI}
153
- />
154
- </SideIcon>
152
+ <div className='control-icon-wrap' title={e('quickConnect')}>
153
+ <ThunderboltOutlined
154
+ className='font20 iblock control-icon'
155
+ />
156
+ </div>
157
+ </Popover>
155
158
  <SideIcon
156
159
  title={e(settingMap.bookmarks)}
157
160
  active={bookmarksActive}