@electerm/electerm-react 5.5.25 → 5.5.35

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 (41) hide show
  1. package/README.md +5 -5
  2. package/client/common/bookmark-schemas.js +3 -0
  3. package/client/common/constants.js +2 -0
  4. package/client/common/create-title.jsx +5 -1
  5. package/client/common/default-setting.js +2 -0
  6. package/client/common/parse-vv.js +119 -25
  7. package/client/common/vv-to-tab.js +120 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +19 -4
  9. package/client/components/ai/agent.js +33 -16
  10. package/client/components/ai/ai-chat-history-item.jsx +12 -48
  11. package/client/components/ai/ai-chat.jsx +82 -25
  12. package/client/components/ai/ai-config-props.js +12 -1
  13. package/client/components/ai/ai-config.jsx +14 -0
  14. package/client/components/ai/ai-context-indicator.jsx +83 -0
  15. package/client/components/ai/ai-context.js +272 -0
  16. package/client/components/ai/ai-output.jsx +4 -0
  17. package/client/components/ai/ai-presets.js +1 -1
  18. package/client/components/ai/ai.styl +105 -6
  19. package/client/components/ai/copy-icon.jsx +31 -0
  20. package/client/components/bookmark-form/common/vv-file-field.jsx +3 -2
  21. package/client/components/bookmark-form/config/spice.js +21 -0
  22. package/client/components/footer/cmd-history.jsx +279 -65
  23. package/client/components/footer/cmd-history.styl +68 -1
  24. package/client/components/footer/footer-entry.jsx +7 -1
  25. package/client/components/layout/layout.jsx +12 -19
  26. package/client/components/main/main.jsx +14 -1
  27. package/client/components/quick-commands/qm.styl +58 -0
  28. package/client/components/quick-commands/quick-commands-box.jsx +180 -83
  29. package/client/components/quick-commands/quick-commands-select.jsx +11 -0
  30. package/client/components/setting-panel/setting-common.jsx +2 -0
  31. package/client/components/side-panel-r/side-panel-r.jsx +11 -4
  32. package/client/components/sidebar/bookmark-select.jsx +14 -2
  33. package/client/components/sidebar/history-item.jsx +20 -4
  34. package/client/components/sidebar/history.jsx +7 -0
  35. package/client/components/tree-list/tree-list.jsx +31 -3
  36. package/client/store/common.js +120 -32
  37. package/client/store/init-state.js +8 -0
  38. package/client/store/load-data.js +72 -0
  39. package/client/store/store.js +6 -0
  40. package/client/store/watch.js +3 -1
  41. package/package.json +1 -1
@@ -26,11 +26,26 @@
26
26
  border-radius 3px
27
27
  pre
28
28
  margin-bottom 0
29
- // .code-block-actions
30
- // display block
31
- // &:hover
32
- // .code-block-actions
33
- // display block
29
+
30
+ // copy icons stay visible all the time, never hover only
31
+ .copy-icon,
32
+ .code-action-icon
33
+ color var(--text-dark)
34
+ font-size 14px
35
+ &:hover
36
+ color var(--primary)
37
+
38
+ .code-block-actions
39
+ display flex
40
+ justify-content flex-end
41
+
42
+ // left aligned on purpose: the streaming stop/loading icon is pinned to the
43
+ // bottom right of .chat-history-item, so a right aligned copy icon would sit
44
+ // on top of it while a response is streaming
45
+ .ai-output-actions
46
+ display flex
47
+ justify-content flex-start
48
+ margin-top 6px
34
49
 
35
50
  .ai-stream-output
36
51
  position relative
@@ -223,9 +238,18 @@
223
238
  font-size 12px
224
239
  color var(--text-color-2)
225
240
 
241
+ .agent-tool-pre-wrap
242
+ position relative
243
+
244
+ .agent-tool-copy
245
+ position absolute
246
+ top 5px
247
+ right 6px
248
+ z-index 1
249
+
226
250
  .agent-tool-pre
227
251
  background var(--main-dark)
228
- padding 6px 8px
252
+ padding 6px 26px 6px 8px
229
253
  border-radius 3px
230
254
  font-size 12px
231
255
  max-height 200px
@@ -313,3 +337,78 @@
313
337
  &:hover
314
338
  border-color var(--primary)
315
339
  color var(--text)
340
+
341
+ // context size indicator, right hand side of the ai chat toolbar
342
+ .ai-context-indicator
343
+ display inline-flex
344
+ align-items center
345
+ gap 4px
346
+ flex-shrink 0
347
+ font-size 11px
348
+ line-height 1
349
+ color var(--text-color-2)
350
+ cursor default
351
+ user-select none
352
+
353
+ .ai-context-bar
354
+ display inline-block
355
+ width 34px
356
+ height 5px
357
+ border-radius 3px
358
+ background var(--main-darker)
359
+ overflow hidden
360
+ flex-shrink 0
361
+
362
+ .ai-context-bar-fill
363
+ display block
364
+ height 100%
365
+ border-radius 3px
366
+ background var(--primary)
367
+ transition width 0.2s
368
+
369
+ .ai-context-count
370
+ color var(--text-dark)
371
+ font-variant-numeric tabular-nums
372
+
373
+ .ai-context-percent
374
+ font-variant-numeric tabular-nums
375
+
376
+ .ai-context-warn
377
+ .ai-context-bar-fill
378
+ background #faad14
379
+ .ai-context-percent
380
+ color #faad14
381
+
382
+ .ai-context-danger
383
+ .ai-context-bar-fill
384
+ background #ff4d4f
385
+ .ai-context-percent
386
+ color #ff4d4f
387
+ font-weight 500
388
+
389
+ .ai-context-tip
390
+ font-size 12px
391
+ min-width 210px
392
+
393
+ .ai-context-tip-title
394
+ font-weight 500
395
+ margin-bottom 4px
396
+
397
+ .ai-context-tip-row
398
+ display flex
399
+ justify-content space-between
400
+ gap 12px
401
+
402
+ .ai-context-tip-total
403
+ font-weight 500
404
+ border-top 1px solid rgba(128, 128, 128, 0.35)
405
+ margin-top 2px
406
+ padding-top 2px
407
+
408
+ .ai-context-tip-note
409
+ margin-top 4px
410
+ opacity 0.75
411
+
412
+ .ai-context-tip-warn
413
+ margin-top 4px
414
+ color #faad14
@@ -0,0 +1,31 @@
1
+ import { CopyOutlined } from '@ant-design/icons'
2
+ import { copy } from '../../common/clipboard'
3
+
4
+ const e = window.translate
5
+
6
+ /**
7
+ * Always visible copy icon, shared by AI output and agent tool call blocks
8
+ * @param {Object} props
9
+ * @param {string} props.text - text to copy
10
+ * @param {string} props.className - extra class for positioning
11
+ * @param {string} props.title - tooltip title
12
+ */
13
+ export default function CopyIcon ({ text, className = '', title }) {
14
+ function handleCopy (evt) {
15
+ // agent tool card header toggles expanded state on click
16
+ evt.stopPropagation()
17
+ copy(text)
18
+ }
19
+
20
+ if (!text) {
21
+ return null
22
+ }
23
+
24
+ return (
25
+ <CopyOutlined
26
+ className={`copy-icon pointer iblock${className ? ' ' + className : ''}`}
27
+ onClick={handleCopy}
28
+ title={title || e('copy')}
29
+ />
30
+ )
31
+ }
@@ -2,8 +2,9 @@
2
2
  * bookmark form - load a .vv (virt-viewer) connection file
3
3
  *
4
4
  * Registered as field type 'vvFile' in ./fields.jsx and used by
5
- * ../config/spice.js. Fills host/port/password/title/proxy from the file and
6
- * reports anything it could not honour instead of dropping it silently.
5
+ * ../config/spice.js. Fills host/port/password/title/proxy/tls/ca/hostSubject
6
+ * from the file and reports anything it could not honour instead of dropping
7
+ * it silently.
7
8
  */
8
9
  import { useState } from 'react'
9
10
  import {
@@ -12,6 +12,9 @@ const spiceConfig = {
12
12
  initValues: (props) => {
13
13
  return createBaseInitValues(props, terminalSpiceType, {
14
14
  port: 5900,
15
+ tls: false,
16
+ ca: '',
17
+ hostSubject: '',
15
18
  viewOnly: false,
16
19
  scaleViewport: true,
17
20
  connectionHoppings: [],
@@ -29,6 +32,24 @@ const spiceConfig = {
29
32
  { type: 'vvFile', name: '__vv__', label: '' },
30
33
  { type: 'input', name: 'host', label: () => e('host'), rules: [{ required: true, message: e('host') + ' required' }] },
31
34
  commonFields.port,
35
+ { type: 'switch', name: 'tls', label: 'TLS', valuePropName: 'checked' },
36
+ {
37
+ type: 'textarea',
38
+ name: 'ca',
39
+ label: 'CA certificate (PEM)',
40
+ props: {
41
+ autoSize: { minRows: 2, maxRows: 8 },
42
+ placeholder: '-----BEGIN CERTIFICATE-----'
43
+ }
44
+ },
45
+ {
46
+ type: 'input',
47
+ name: 'hostSubject',
48
+ label: 'Certificate subject',
49
+ props: {
50
+ placeholder: 'CN=pve1,O=Proxmox Virtual Environment'
51
+ }
52
+ },
32
53
  { type: 'switch', name: 'viewOnly', label: () => e('viewOnly'), valuePropName: 'checked' },
33
54
  { type: 'switch', name: 'scaleViewport', label: () => e('scaleViewport'), valuePropName: 'checked' },
34
55
  { type: 'profileItem', name: '__profile__', label: '', profileFilter: d => !isEmpty(d.spice) },
@@ -1,9 +1,14 @@
1
1
  /**
2
- * cmd history trigger button with popover
2
+ * cmd history panel with popover
3
+ *
4
+ * The panel has two homes: the footer popover (default) and the right side
5
+ * panel. Which one it is in is a stored preference (store.cmdHistoryInRightPanel),
6
+ * so the same component renders both — `inline` drops the trigger/popover and
7
+ * lets the right panel's own container wrap the panel body.
3
8
  */
4
9
 
5
- import { useState, useEffect } from 'react'
6
- import { Button, Empty, Popover, Dropdown } from 'antd'
10
+ import { useState, useEffect, useLayoutEffect, useRef } from 'react'
11
+ import { Button, Empty, Popover } from 'antd'
7
12
  import { auto } from 'manate/react'
8
13
  import SwitchLabel from '../common/switch'
9
14
  import { copy } from '../../common/clipboard'
@@ -14,12 +19,15 @@ import {
14
19
  UnorderedListOutlined,
15
20
  MoreOutlined,
16
21
  PlusOutlined,
17
- CodeOutlined
22
+ CodeOutlined,
23
+ VerticalLeftOutlined,
24
+ VerticalAlignBottomOutlined
18
25
  } from '@ant-design/icons'
19
26
  import InputAutoFocus from '../common/input-auto-focus'
20
27
  import { getItemJSON, setItemJSON } from '../../common/safe-local-storage'
21
28
  import QuickCommandCreateModal from '../quick-commands/quick-command-create-modal'
22
29
  import MultiTabRunModal from './multi-tab-run-modal'
30
+ import { refsStatic } from '../common/ref'
23
31
  import classNames from 'classnames'
24
32
  import './cmd-history.styl'
25
33
 
@@ -27,21 +35,71 @@ const e = window.translate
27
35
  const SORT_BY_FREQ_KEY = 'electerm-cmd-history-sort-by-frequency'
28
36
 
29
37
  export default auto(function CmdHistory (props) {
38
+ const { store, inline } = props
30
39
  const [keyword, setKeyword] = useState('')
31
40
  const [sortByFrequency, setSortByFrequency] = useState(() => {
32
41
  return getItemJSON(SORT_BY_FREQ_KEY, false)
33
42
  })
34
- // cmd of the item whose action menu is open / being acted on
35
- const [menuOpenCmd, setMenuOpenCmd] = useState('')
43
+ // The item action menu is a part of this panel, not of the row it acts on:
44
+ // the row's ⋯ icon only reports which command was picked and where that row
45
+ // sits ({ cmd, top, rowTop }), and the panel renders the single menu for it.
46
+ // A menu per row cannot be closed with the panel — the popover content stops
47
+ // being re-rendered the moment the popover closes, so a per-row menu portaled
48
+ // to the body is left floating on its own.
49
+ const [menu, setMenu] = useState(null)
50
+ const panelRef = useRef(null)
51
+ const menuRef = useRef(null)
36
52
  const [quickCommandCmd, setQuickCommandCmd] = useState('')
37
53
  const [multiTabCmd, setMultiTabCmd] = useState('')
38
- const { terminalCommandHistory } = props.store
54
+ // Footer mode only. The popover is controlled so that the trigger can be
55
+ // repurposed (open the right panel instead, when the history lives there) and
56
+ // so that "move back to footer" can open it again from the store.
57
+ const [open, setOpen] = useState(false)
58
+ const { terminalCommandHistory } = store
39
59
 
40
60
  useEffect(() => {
41
61
  setItemJSON(SORT_BY_FREQ_KEY, sortByFrequency)
42
62
  }, [sortByFrequency])
43
63
 
64
+ // "move back to footer" happens in the right panel, which cannot reach this
65
+ // component through props — it goes through the store, which needs a handle
66
+ useEffect(() => {
67
+ if (inline) {
68
+ return
69
+ }
70
+ refsStatic.add('CmdHistory', {
71
+ openPopover: () => setOpen(true)
72
+ })
73
+ return () => refsStatic.remove('CmdHistory')
74
+ }, [inline])
75
+
76
+ // The menu hangs below its row, which for the last rows would put it past the
77
+ // bottom of the window — the panel itself is not clipped, so lift the menu
78
+ // above its row instead. The layout effect runs before paint, so the
79
+ // correction is never visible.
80
+ useLayoutEffect(() => {
81
+ const el = menuRef.current
82
+ if (!el || !menu) {
83
+ return
84
+ }
85
+ const rect = el.getBoundingClientRect()
86
+ if (rect.bottom > window.innerHeight - 4) {
87
+ const above = menu.rowTop - rect.height
88
+ if (above !== menu.top) {
89
+ setMenu({
90
+ ...menu,
91
+ top: above
92
+ })
93
+ }
94
+ }
95
+ }, [menu])
96
+
97
+ function closeMenu () {
98
+ setMenu(null)
99
+ }
100
+
44
101
  function handleRunCommand (cmd) {
102
+ closeMenu()
45
103
  window.store.runCmdFromHistory(cmd)
46
104
  }
47
105
 
@@ -55,6 +113,7 @@ export default auto(function CmdHistory (props) {
55
113
  }
56
114
 
57
115
  function handleMenuAction (key, cmd) {
116
+ closeMenu()
58
117
  if (key === 'delete') {
59
118
  window.store.deleteCmdHistory(cmd)
60
119
  } else if (key === 'quickCommand') {
@@ -65,33 +124,49 @@ export default auto(function CmdHistory (props) {
65
124
  }
66
125
  }
67
126
 
68
- function getMenuProps (cmd) {
69
- return {
70
- items: [
71
- {
72
- key: 'delete',
73
- icon: <DeleteOutlined />,
74
- label: e('del'),
75
- danger: true
76
- },
77
- {
78
- key: 'quickCommand',
79
- icon: <PlusOutlined />,
80
- label: e('addQuickCommands')
81
- },
82
- {
83
- key: 'multiTab',
84
- icon: <CodeOutlined />,
85
- label: e('runInAllTerminals')
86
- }
87
- ],
88
- onClick: ({ key, domEvent }) => {
89
- // the menu renders in a portal but React events still bubble through
90
- // the row, which would run the command
91
- domEvent.stopPropagation()
92
- handleMenuAction(key, cmd)
127
+ function getMenuItems () {
128
+ return [
129
+ {
130
+ key: 'delete',
131
+ icon: <DeleteOutlined />,
132
+ label: e('del'),
133
+ danger: true
134
+ },
135
+ {
136
+ key: 'quickCommand',
137
+ icon: <PlusOutlined />,
138
+ label: e('addQuickCommands')
139
+ },
140
+ {
141
+ key: 'multiTab',
142
+ icon: <CodeOutlined />,
143
+ label: e('runInAllTerminals')
93
144
  }
145
+ ]
146
+ }
147
+
148
+ // the ⋯ icon only reports the picked command and its position; the panel owns
149
+ // the menu itself
150
+ function handleToggleMenu (ev, cmd) {
151
+ // the row would otherwise run the command
152
+ ev.stopPropagation()
153
+ if (menu && menu.cmd === cmd) {
154
+ closeMenu()
155
+ return
156
+ }
157
+ const row = ev.currentTarget.closest('.cmd-history-item')
158
+ const panel = panelRef.current
159
+ if (!row || !panel) {
160
+ return
94
161
  }
162
+ const rowRect = row.getBoundingClientRect()
163
+ const panelTop = panel.getBoundingClientRect().top
164
+ setMenu({
165
+ cmd,
166
+ // below the row by default, above it when there is no room left
167
+ top: rowRect.bottom - panelTop,
168
+ rowTop: rowRect.top - panelTop
169
+ })
95
170
  }
96
171
 
97
172
  function filterArray (array, keyword) {
@@ -105,6 +180,45 @@ export default auto(function CmdHistory (props) {
105
180
  setKeyword(e.target.value)
106
181
  }
107
182
 
183
+ // The menu is anchored to its row, and rc-trigger re-aligns it on every
184
+ // scroll of the trigger's scrollable ancestors. The rows live in a 190px-tall
185
+ // scrolling list, so a wheel over the list drags an open menu along with the
186
+ // row — out of the popup and eventually off the screen, while the row it
187
+ // belongs to is long gone. Close it instead: the menu is only meaningful next
188
+ // to the row it was opened on.
189
+ function handleListScroll () {
190
+ if (menu) {
191
+ closeMenu()
192
+ }
193
+ }
194
+
195
+ // closing the popover hides the menu with it; dropping the selection too
196
+ // keeps it from coming back on the next open
197
+ function handlePopoverOpenChange (nextOpen) {
198
+ if (!nextOpen) {
199
+ closeMenu()
200
+ setOpen(false)
201
+ return
202
+ }
203
+ // While the history lives in the right panel this trigger never opens the
204
+ // popover: it opens and closes the right panel, exactly like the info and
205
+ // AI triggers do (store.toggleCmdHistoryPanel).
206
+ if (store.cmdHistoryInRightPanel) {
207
+ window.store.toggleCmdHistoryPanel()
208
+ return
209
+ }
210
+ setOpen(true)
211
+ }
212
+
213
+ // Dock the panel into the right side panel. The popover has to be closed on
214
+ // the way out, or it would be left behind pointing at a panel that is no
215
+ // longer in the footer.
216
+ function handleMoveToRightPanel () {
217
+ closeMenu()
218
+ setOpen(false)
219
+ window.store.moveCmdHistoryToRightPanel()
220
+ }
221
+
108
222
  const historyArray = (terminalCommandHistory || []).slice().reverse()
109
223
 
110
224
  let filtered = filterArray(historyArray, keyword)
@@ -129,7 +243,7 @@ export default auto(function CmdHistory (props) {
129
243
  return filtered.map((item, index) => {
130
244
  const cls = classNames(
131
245
  'cmd-history-item',
132
- { 'menu-open': menuOpenCmd === item.cmd }
246
+ { 'menu-open': menu && menu.cmd === item.cmd }
133
247
  )
134
248
  return (
135
249
  <div
@@ -149,25 +263,80 @@ export default auto(function CmdHistory (props) {
149
263
  className='cmd-history-item-copy'
150
264
  onClick={(ev) => handleCopyCommand(item.cmd, ev)}
151
265
  />
152
- <Dropdown
153
- menu={getMenuProps(item.cmd)}
154
- trigger={['click']}
155
- onOpenChange={(open) => setMenuOpenCmd(open ? item.cmd : '')}
156
- >
157
- <Button
158
- type='text'
159
- size='small'
160
- icon={<MoreOutlined />}
161
- className='cmd-history-item-more'
162
- onClick={(ev) => ev.stopPropagation()}
163
- />
164
- </Dropdown>
266
+ <Button
267
+ type='text'
268
+ size='small'
269
+ icon={<MoreOutlined />}
270
+ className={classNames(
271
+ 'cmd-history-item-more',
272
+ { active: menu && menu.cmd === item.cmd }
273
+ )}
274
+ onClick={(ev) => handleToggleMenu(ev, item.cmd)}
275
+ />
165
276
  </div>
166
277
  </div>
167
278
  )
168
279
  })
169
280
  }
170
281
 
282
+ // the panel's own action menu: it renders inside the popover content, so it
283
+ // is hidden and revealed with the panel instead of outliving it
284
+ function renderItemMenu () {
285
+ if (!menu) {
286
+ return null
287
+ }
288
+ return (
289
+ <div
290
+ ref={menuRef}
291
+ className='cmd-history-item-menu'
292
+ style={{ top: `${menu.top}px` }}
293
+ onClick={ev => ev.stopPropagation()}
294
+ >
295
+ {
296
+ getMenuItems().map(item => (
297
+ <div
298
+ key={item.key}
299
+ className={classNames(
300
+ 'cmd-history-menu-item',
301
+ { danger: item.danger }
302
+ )}
303
+ onClick={() => handleMenuAction(item.key, menu.cmd)}
304
+ >
305
+ <span className='cmd-history-menu-icon'>{item.icon}</span>
306
+ <span className='cmd-history-menu-label'>{item.label}</span>
307
+ </div>
308
+ ))
309
+ }
310
+ </div>
311
+ )
312
+ }
313
+
314
+ // The two "move" icons are mirror images and sit in the same slot: the
315
+ // popover offers to dock the panel into the right side panel, the docked
316
+ // panel offers to hand it back to the footer popover. The glyphs are antd's
317
+ // "align" pair — an arrow into a bar on the right, an arrow into a bar at the
318
+ // bottom — so each points at the edge the panel is going to.
319
+ // Note VerticalLeftOutlined is the one whose bar is on the right (arrow
320
+ // pointing right); VerticalRightOutlined is its mirror and points left.
321
+ function renderMoveIcon () {
322
+ if (inline) {
323
+ return (
324
+ <VerticalAlignBottomOutlined
325
+ className='cmd-history-move-icon pointer'
326
+ title={e('moveToFooter')}
327
+ onClick={() => window.store.moveCmdHistoryToFooter()}
328
+ />
329
+ )
330
+ }
331
+ return (
332
+ <VerticalLeftOutlined
333
+ className='cmd-history-move-icon pointer'
334
+ title={e('moveToRightPanel')}
335
+ onClick={handleMoveToRightPanel}
336
+ />
337
+ )
338
+ }
339
+
171
340
  function renderHeader () {
172
341
  if (!historyArray.length) {
173
342
  return null
@@ -182,17 +351,29 @@ export default auto(function CmdHistory (props) {
182
351
  label={e('sortByFrequency')}
183
352
  />
184
353
  </div>
185
- <UnorderedListOutlined
186
- className='cmd-history-clear-icon pointer clear-ai-icon icon-hover'
187
- title={e('clear')}
188
- onClick={handleClearAll}
189
- />
354
+ <div className='cmd-history-header-actions'>
355
+ {renderMoveIcon()}
356
+ <UnorderedListOutlined
357
+ className='cmd-history-clear-icon pointer clear-ai-icon icon-hover'
358
+ title={e('clear')}
359
+ onClick={handleClearAll}
360
+ />
361
+ </div>
190
362
  </div>
191
363
  )
192
364
  }
193
365
 
194
366
  const content = (
195
- <div className='cmd-history-popover-content pd2'>
367
+ <div
368
+ ref={panelRef}
369
+ className={classNames(
370
+ 'cmd-history-popover-content',
371
+ inline
372
+ // docked: fill the right panel instead of sizing itself like a popup
373
+ ? 'cmd-history-in-panel'
374
+ : 'pd2'
375
+ )}
376
+ >
196
377
  <div className='cmd-history-search pd2b'>
197
378
  <InputAutoFocus
198
379
  value={keyword}
@@ -203,25 +384,18 @@ export default auto(function CmdHistory (props) {
203
384
  />
204
385
  </div>
205
386
  {renderHeader()}
206
- <div className='cmd-history-list'>
387
+ <div className='cmd-history-list' onScroll={handleListScroll}>
207
388
  {renderList()}
208
389
  </div>
390
+ {renderItemMenu()}
209
391
  </div>
210
392
  )
211
393
 
212
- return (
394
+ // The modals are siblings of the panel, never children of the popover: the
395
+ // content of a closed popover is no longer re-rendered, so a modal opened
396
+ // from inside one could not be closed once the popover went away.
397
+ const modals = (
213
398
  <>
214
- <Popover
215
- content={content}
216
- trigger='click'
217
- placement='topLeft'
218
- >
219
- <Button
220
- size='small'
221
- type='text'
222
- icon={<HistoryOutlined />}
223
- />
224
- </Popover>
225
399
  {
226
400
  quickCommandCmd
227
401
  ? (
@@ -247,4 +421,44 @@ export default auto(function CmdHistory (props) {
247
421
  }
248
422
  </>
249
423
  )
424
+
425
+ // docked in the right side panel: no trigger, no popover — the right panel
426
+ // decides when this is on screen, via store.rightPanelTab
427
+ if (inline) {
428
+ if (store.rightPanelTab !== 'cmdHistory') {
429
+ return null
430
+ }
431
+ return (
432
+ <>
433
+ {content}
434
+ {modals}
435
+ </>
436
+ )
437
+ }
438
+
439
+ const rightPanelActive = store.rightPanelVisible &&
440
+ store.rightPanelTab === 'cmdHistory'
441
+
442
+ return (
443
+ <>
444
+ <Popover
445
+ content={content}
446
+ trigger='click'
447
+ placement='topLeft'
448
+ open={open}
449
+ onOpenChange={handlePopoverOpenChange}
450
+ >
451
+ <Button
452
+ size='small'
453
+ type='text'
454
+ className={classNames(
455
+ 'cmd-history-trigger',
456
+ { active: rightPanelActive }
457
+ )}
458
+ icon={<HistoryOutlined />}
459
+ />
460
+ </Popover>
461
+ {modals}
462
+ </>
463
+ )
250
464
  })