@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.
- package/README.md +5 -5
- package/client/common/bookmark-schemas.js +3 -0
- package/client/common/constants.js +2 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +2 -0
- package/client/common/parse-vv.js +119 -25
- package/client/common/vv-to-tab.js +120 -0
- package/client/components/ai/agent-tool-call-card.jsx +19 -4
- package/client/components/ai/agent.js +33 -16
- package/client/components/ai/ai-chat-history-item.jsx +12 -48
- package/client/components/ai/ai-chat.jsx +82 -25
- package/client/components/ai/ai-config-props.js +12 -1
- package/client/components/ai/ai-config.jsx +14 -0
- package/client/components/ai/ai-context-indicator.jsx +83 -0
- package/client/components/ai/ai-context.js +272 -0
- package/client/components/ai/ai-output.jsx +4 -0
- package/client/components/ai/ai-presets.js +1 -1
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +3 -2
- package/client/components/bookmark-form/config/spice.js +21 -0
- package/client/components/footer/cmd-history.jsx +279 -65
- package/client/components/footer/cmd-history.styl +68 -1
- package/client/components/footer/footer-entry.jsx +7 -1
- package/client/components/layout/layout.jsx +12 -19
- package/client/components/main/main.jsx +14 -1
- package/client/components/quick-commands/qm.styl +58 -0
- package/client/components/quick-commands/quick-commands-box.jsx +180 -83
- package/client/components/quick-commands/quick-commands-select.jsx +11 -0
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/side-panel-r/side-panel-r.jsx +11 -4
- package/client/components/sidebar/bookmark-select.jsx +14 -2
- package/client/components/sidebar/history-item.jsx +20 -4
- package/client/components/sidebar/history.jsx +7 -0
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/store/common.js +120 -32
- package/client/store/init-state.js +8 -0
- package/client/store/load-data.js +72 -0
- package/client/store/store.js +6 -0
- package/client/store/watch.js +3 -1
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@ import './right-side-panel.styl'
|
|
|
4
4
|
import {
|
|
5
5
|
CloseCircleOutlined,
|
|
6
6
|
PushpinOutlined,
|
|
7
|
-
BarChartOutlined
|
|
7
|
+
BarChartOutlined,
|
|
8
|
+
HistoryOutlined,
|
|
9
|
+
ThunderboltOutlined
|
|
8
10
|
} from '@ant-design/icons'
|
|
9
11
|
import {
|
|
10
12
|
Typography,
|
|
@@ -57,9 +59,14 @@ export default memo(function RightSidePanel (
|
|
|
57
59
|
// from a desktop session must not turn the panel into a dock here — that
|
|
58
60
|
// would make it full height over the footer for no reachable reason.
|
|
59
61
|
const pinned = rightPanelPinned && !isMobile
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// one glyph per tab, so the panel says what it is showing; the info tab is
|
|
63
|
+
// the fallback, since a stale or unknown tab falls back to it everywhere else
|
|
64
|
+
const tagMap = {
|
|
65
|
+
ai: <Tag className='mg1r'>AI</Tag>,
|
|
66
|
+
cmdHistory: <HistoryOutlined className='mg1r' />,
|
|
67
|
+
quickCommands: <ThunderboltOutlined className='mg1r' />
|
|
68
|
+
}
|
|
69
|
+
const tag = tagMap[rightPanelTab] || <BarChartOutlined className='mg1r' />
|
|
63
70
|
|
|
64
71
|
function onDragEnd (nw) {
|
|
65
72
|
window.store.setRightSidePanelWidth(nw)
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* bookmark select
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react'
|
|
5
6
|
import { auto } from 'manate/react'
|
|
6
7
|
import TreeList from '../tree-list/tree-list'
|
|
7
8
|
|
|
8
9
|
export default auto(function BookmarkSelect (props) {
|
|
9
10
|
const { store, from, autoFocus } = props
|
|
11
|
+
const [activeItemId, setActiveItemId] = useState('')
|
|
10
12
|
const {
|
|
11
13
|
listStyle,
|
|
12
14
|
openedSideBar,
|
|
@@ -19,7 +21,7 @@ export default auto(function BookmarkSelect (props) {
|
|
|
19
21
|
if (from === 'sidebar' && openedSideBar !== 'bookmarks') {
|
|
20
22
|
return null
|
|
21
23
|
}
|
|
22
|
-
const
|
|
24
|
+
const openBookmark = (item) => {
|
|
23
25
|
// A pinned (docked) panel stays open; anything else — including mobile,
|
|
24
26
|
// where pinning is not a reachable mode — dismisses on selection.
|
|
25
27
|
if (!(store.pinned && !store.isMobile)) {
|
|
@@ -27,10 +29,20 @@ export default auto(function BookmarkSelect (props) {
|
|
|
27
29
|
}
|
|
28
30
|
store.onSelectBookmark(item.id)
|
|
29
31
|
}
|
|
32
|
+
// doubleClickToOpenBookmark: a single click only marks the row, the session
|
|
33
|
+
// opens on a double click. Touch devices have no double click, so the setting
|
|
34
|
+
// is ignored there. This is the sidebar list only — the settings bookmarks tab
|
|
35
|
+
// keeps selecting for editing on a single click.
|
|
36
|
+
const openOnDoubleClick = !!store.config.doubleClickToOpenBookmark &&
|
|
37
|
+
!store.isTouchDevice
|
|
30
38
|
const base = {
|
|
31
39
|
bookmarks: bookmarks || [],
|
|
32
40
|
type: 'bookmarks',
|
|
33
|
-
|
|
41
|
+
activeItemId: openOnDoubleClick ? activeItemId : undefined,
|
|
42
|
+
onClickItem: openOnDoubleClick
|
|
43
|
+
? item => setActiveItemId(item.id)
|
|
44
|
+
: openBookmark,
|
|
45
|
+
onDoubleClickItem: openOnDoubleClick ? openBookmark : undefined,
|
|
34
46
|
listStyle,
|
|
35
47
|
staticList: true
|
|
36
48
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef } from 'react'
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import classnames from 'classnames'
|
|
2
3
|
import createTitle, { createTitleWithTag } from '../../common/create-title'
|
|
3
4
|
import { DeleteOutlined, BookFilled } from '@ant-design/icons'
|
|
4
5
|
import { refsStatic } from '../common/ref'
|
|
@@ -6,18 +7,32 @@ import { refsStatic } from '../common/ref'
|
|
|
6
7
|
export default function HistoryItem (props) {
|
|
7
8
|
const { store } = window
|
|
8
9
|
const {
|
|
9
|
-
item
|
|
10
|
+
item,
|
|
11
|
+
openOnDoubleClick
|
|
10
12
|
} = props
|
|
13
|
+
const [active, setActive] = useState(false)
|
|
11
14
|
const timeoutRef = useRef(null)
|
|
12
15
|
|
|
13
16
|
const handleClick = useCallback(() => {
|
|
17
|
+
// with doubleClickToOpenBookmark on, a click only marks the row — the
|
|
18
|
+
// session opens on the double click below
|
|
19
|
+
if (openOnDoubleClick) {
|
|
20
|
+
setActive(true)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
14
23
|
if (timeoutRef.current) {
|
|
15
24
|
clearTimeout(timeoutRef.current)
|
|
16
25
|
}
|
|
17
26
|
timeoutRef.current = setTimeout(() => {
|
|
18
27
|
store.onSelectHistory(item.tab)
|
|
19
28
|
}, 10)
|
|
20
|
-
}, [item.tab])
|
|
29
|
+
}, [item.tab, openOnDoubleClick])
|
|
30
|
+
|
|
31
|
+
const handleDoubleClick = useCallback(() => {
|
|
32
|
+
if (openOnDoubleClick) {
|
|
33
|
+
store.onSelectHistory(item.tab)
|
|
34
|
+
}
|
|
35
|
+
}, [item.tab, openOnDoubleClick])
|
|
21
36
|
|
|
22
37
|
useEffect(() => {
|
|
23
38
|
return () => {
|
|
@@ -47,9 +62,10 @@ export default function HistoryItem (props) {
|
|
|
47
62
|
const tt = createTitle(item.tab)
|
|
48
63
|
return (
|
|
49
64
|
<div
|
|
50
|
-
className='item-list-unit'
|
|
65
|
+
className={classnames('item-list-unit', { active })}
|
|
51
66
|
title={tt}
|
|
52
67
|
onClick={handleClick}
|
|
68
|
+
onDoubleClick={handleDoubleClick}
|
|
53
69
|
>
|
|
54
70
|
<div className='elli pd1y pd2x'>
|
|
55
71
|
{title}
|
|
@@ -25,6 +25,12 @@ export default auto(function HistoryPanel (props) {
|
|
|
25
25
|
const {
|
|
26
26
|
history
|
|
27
27
|
} = store
|
|
28
|
+
// doubleClickToOpenBookmark: a single click only marks the row, the session
|
|
29
|
+
// opens on a double click. Touch devices have no double click, so the setting
|
|
30
|
+
// is ignored there. Read here rather than in HistoryItem because this
|
|
31
|
+
// component is `auto` — it re-renders when the input modality flips.
|
|
32
|
+
const openOnDoubleClick = !!store.config.doubleClickToOpenBookmark &&
|
|
33
|
+
!store.isTouchDevice
|
|
28
34
|
let arr = store.config.disableConnectionHistory ? [] : history
|
|
29
35
|
if (sortByFrequency) {
|
|
30
36
|
arr = [...arr].sort((a, b) => { return b.count - a.count })
|
|
@@ -75,6 +81,7 @@ export default auto(function HistoryPanel (props) {
|
|
|
75
81
|
<HistoryItem
|
|
76
82
|
key={item.id}
|
|
77
83
|
item={item}
|
|
84
|
+
openOnDoubleClick={openOnDoubleClick}
|
|
78
85
|
/>
|
|
79
86
|
)
|
|
80
87
|
})
|
|
@@ -158,7 +158,11 @@ export default class ItemListTree extends Component {
|
|
|
158
158
|
: matchedRowKeys[0]
|
|
159
159
|
const row = rows.find(item => item.key === rowKey)
|
|
160
160
|
if (row?.item?.id) {
|
|
161
|
-
|
|
161
|
+
// Enter is a deliberate activation, so it opens the bookmark even when
|
|
162
|
+
// doubleClickToOpenBookmark makes a single click only mark the row. The
|
|
163
|
+
// settings bookmarks tab passes no onDoubleClickItem, so there Enter
|
|
164
|
+
// still selects for editing, exactly like a click.
|
|
165
|
+
this.selectBookmarkById(row.item.id, this.props.onDoubleClickItem)
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
}
|
|
@@ -404,7 +408,9 @@ export default class ItemListTree extends Component {
|
|
|
404
408
|
}
|
|
405
409
|
}
|
|
406
410
|
|
|
407
|
-
|
|
411
|
+
// `act` overrides what a selection does; only the Enter path passes it, so a
|
|
412
|
+
// click keeps whatever meaning the consumer gave onClickItem.
|
|
413
|
+
selectBookmarkById = (id, act) => {
|
|
408
414
|
const { store } = window
|
|
409
415
|
store.storeAssign({
|
|
410
416
|
currentBookmarkGroupId: findBookmarkGroupId(store.bookmarkGroups, id)
|
|
@@ -414,7 +420,28 @@ export default class ItemListTree extends Component {
|
|
|
414
420
|
d => d.id === id
|
|
415
421
|
)
|
|
416
422
|
if (bookmark) {
|
|
417
|
-
this.props.onClickItem(bookmark)
|
|
423
|
+
(act || this.props.onClickItem)(bookmark)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
handleDoubleClick = (e) => {
|
|
428
|
+
// only the static list (the sidebar) passes this, so the bookmark edit page
|
|
429
|
+
// keeps its single click to select behaviour
|
|
430
|
+
const { onDoubleClickItem, bookmarks } = this.props
|
|
431
|
+
if (!onDoubleClickItem) {
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
const item = findParentBySel(e.target, '.tree-item')
|
|
435
|
+
// a group toggles on a single click, so a double click would just toggle it
|
|
436
|
+
// twice and land back where it started
|
|
437
|
+
if (!item || item.getAttribute('data-is-group') === 'true') {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
const bookmark = bookmarks.find(
|
|
441
|
+
d => d.id === item.getAttribute('data-item-id')
|
|
442
|
+
)
|
|
443
|
+
if (bookmark) {
|
|
444
|
+
onDoubleClickItem(bookmark)
|
|
418
445
|
}
|
|
419
446
|
}
|
|
420
447
|
|
|
@@ -917,6 +944,7 @@ export default class ItemListTree extends Component {
|
|
|
917
944
|
className='item-list-wrap'
|
|
918
945
|
style={listStyle}
|
|
919
946
|
ref={this.listRef}
|
|
947
|
+
onDoubleClick={this.handleDoubleClick}
|
|
920
948
|
>
|
|
921
949
|
{this.renderVirtualTreeContent(rows, editor)}
|
|
922
950
|
{this.renderEditorOverlay(editor)}
|
package/client/store/common.js
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
import handleError from '../common/error-handler'
|
|
6
6
|
import Modal from '../components/common/modal'
|
|
7
7
|
import { appendMandatoryGuardrails } from '../components/ai/ai-guardrails'
|
|
8
|
+
import { buildSessionMessages } from '../components/ai/ai-context'
|
|
8
9
|
import { debounce, some, get, pickBy } from 'lodash-es'
|
|
9
10
|
import {
|
|
10
11
|
leftSidePanelWidthKey,
|
|
11
12
|
leftSideBarOpenKey,
|
|
12
13
|
rightSidebarWidthKey,
|
|
13
14
|
rightPanelPinnedKey,
|
|
15
|
+
cmdHistoryInRightPanelKey,
|
|
16
|
+
quickCommandsInRightPanelKey,
|
|
14
17
|
addPanelWidthLsKey,
|
|
15
18
|
connectionMap,
|
|
16
19
|
lastAiChatSessionIdKey,
|
|
@@ -25,7 +28,7 @@ import { requireTermOfUse } from '../common/term-of-use'
|
|
|
25
28
|
import { action } from 'manate'
|
|
26
29
|
import uid from '../common/uid'
|
|
27
30
|
import deepCopy from 'json-deep-copy'
|
|
28
|
-
import { aiConfigsArr } from '../components/ai/ai-config-props'
|
|
31
|
+
import { aiConfigsArr, optionalAIConfigsArr } from '../components/ai/ai-config-props'
|
|
29
32
|
|
|
30
33
|
const e = window.translate
|
|
31
34
|
const { assign } = Object
|
|
@@ -179,6 +182,100 @@ export default Store => {
|
|
|
179
182
|
ls.setItem(rightPanelPinnedKey, v + '')
|
|
180
183
|
window.store.rightPanelPinned = v
|
|
181
184
|
}
|
|
185
|
+
|
|
186
|
+
// The cmd history panel has two homes: the footer popover (the default) and
|
|
187
|
+
// the right side panel. Which one it is in is a durable preference, persisted
|
|
188
|
+
// like the right panel pin — the footer trigger has to keep landing where the
|
|
189
|
+
// user last put the panel, or the choice would only survive until the next
|
|
190
|
+
// reload.
|
|
191
|
+
Store.prototype.setCmdHistoryInRightPanel = function (v) {
|
|
192
|
+
ls.setItem(cmdHistoryInRightPanelKey, v + '')
|
|
193
|
+
window.store.cmdHistoryInRightPanel = v
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Store.prototype.openCmdHistoryPanel = function () {
|
|
197
|
+
const { store } = window
|
|
198
|
+
store.rightPanelVisible = true
|
|
199
|
+
store.rightPanelTab = 'cmdHistory'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Same toggle contract as toggleInfoPanel/toggleAIPanel: while the history
|
|
203
|
+
// lives in the right panel, the footer trigger opens and closes it.
|
|
204
|
+
Store.prototype.toggleCmdHistoryPanel = function () {
|
|
205
|
+
const { store } = window
|
|
206
|
+
if (store.rightPanelVisible && store.rightPanelTab === 'cmdHistory') {
|
|
207
|
+
store.rightPanelVisible = false
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
store.openCmdHistoryPanel()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Store.prototype.moveCmdHistoryToRightPanel = function () {
|
|
214
|
+
const { store } = window
|
|
215
|
+
store.setCmdHistoryInRightPanel(true)
|
|
216
|
+
store.openCmdHistoryPanel()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Hand the panel back to the footer popover and open it there: the move has
|
|
220
|
+
// to be visible, otherwise the panel just looks like it vanished.
|
|
221
|
+
Store.prototype.moveCmdHistoryToFooter = function () {
|
|
222
|
+
const { store } = window
|
|
223
|
+
store.setCmdHistoryInRightPanel(false)
|
|
224
|
+
if (store.rightPanelTab === 'cmdHistory') {
|
|
225
|
+
store.rightPanelVisible = false
|
|
226
|
+
}
|
|
227
|
+
refsStatic.get('CmdHistory')?.openPopover()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The quick command panel has the same two homes as the cmd history one, and
|
|
231
|
+
// the same durable preference: the footer popup (the default) and the right
|
|
232
|
+
// side panel. The footer popup is not a popover but a floating box driven by
|
|
233
|
+
// store.openQuickCommandBar, so "docked" is expressed by the preference alone
|
|
234
|
+
// and the box itself decides which of its two forms to render.
|
|
235
|
+
Store.prototype.setQuickCommandsInRightPanel = function (v) {
|
|
236
|
+
ls.setItem(quickCommandsInRightPanelKey, v + '')
|
|
237
|
+
window.store.quickCommandsInRightPanel = v
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Store.prototype.openQuickCommandsPanel = function () {
|
|
241
|
+
const { store } = window
|
|
242
|
+
store.rightPanelVisible = true
|
|
243
|
+
store.rightPanelTab = 'quickCommands'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Same toggle contract as toggleInfoPanel/toggleAIPanel/toggleCmdHistoryPanel:
|
|
247
|
+
// while the panel lives in the right panel, the footer Q opens and closes it.
|
|
248
|
+
Store.prototype.toggleQuickCommandsPanel = function () {
|
|
249
|
+
const { store } = window
|
|
250
|
+
if (store.rightPanelVisible && store.rightPanelTab === 'quickCommands') {
|
|
251
|
+
store.rightPanelVisible = false
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
store.openQuickCommandsPanel()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
Store.prototype.moveQuickCommandsToRightPanel = function () {
|
|
258
|
+
const { store } = window
|
|
259
|
+
store.setQuickCommandsInRightPanel(true)
|
|
260
|
+
// the footer box would otherwise be left floating over a terminal it no
|
|
261
|
+
// longer owns; the pin is deliberately kept, so handing the panel back
|
|
262
|
+
// restores the exact shape it had before
|
|
263
|
+
store.openQuickCommandBar = false
|
|
264
|
+
store.openQuickCommandsPanel()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Hand the panel back to the footer and open it there: the move has to be
|
|
268
|
+
// visible, otherwise the panel just looks like it vanished. A pin that was
|
|
269
|
+
// left on while docked brings the box back pinned, which is what the user
|
|
270
|
+
// last asked for.
|
|
271
|
+
Store.prototype.moveQuickCommandsToFooter = function () {
|
|
272
|
+
const { store } = window
|
|
273
|
+
store.setQuickCommandsInRightPanel(false)
|
|
274
|
+
if (store.rightPanelTab === 'quickCommands') {
|
|
275
|
+
store.rightPanelVisible = false
|
|
276
|
+
}
|
|
277
|
+
store.openQuickCommandBar = true
|
|
278
|
+
}
|
|
182
279
|
Store.prototype.beforeExit = function (evt) {
|
|
183
280
|
const { confirmBeforeExit } = window.store.config
|
|
184
281
|
if (
|
|
@@ -353,7 +450,10 @@ export default Store => {
|
|
|
353
450
|
if (index === -1) {
|
|
354
451
|
return
|
|
355
452
|
}
|
|
356
|
-
|
|
453
|
+
// Reassign instead of splice: the store only notifies subscribers on a
|
|
454
|
+
// property write, so an in-place mutation leaves the panel rendering the
|
|
455
|
+
// list it already had.
|
|
456
|
+
window.store.aiChatHistory = store.aiChatHistory.filter(d => d.id !== id)
|
|
357
457
|
}
|
|
358
458
|
|
|
359
459
|
Store.prototype.startNewChat = action(function () {
|
|
@@ -388,6 +488,9 @@ export default Store => {
|
|
|
388
488
|
|
|
389
489
|
Store.prototype.compressChatSession = async function (sessionId) {
|
|
390
490
|
const { store } = window
|
|
491
|
+
if (!sessionId) {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
391
494
|
const sessionEntries = store.aiChatHistory
|
|
392
495
|
.filter(h => h.chatSessionId === sessionId)
|
|
393
496
|
.sort((a, b) => a.timestamp - b.timestamp)
|
|
@@ -411,33 +514,14 @@ export default Store => {
|
|
|
411
514
|
|
|
412
515
|
const firstEntry = sessionEntries[0]
|
|
413
516
|
const lang = firstEntry.languageAI || store.getLangName()
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (entry.compressed) {
|
|
423
|
-
messages.push({
|
|
424
|
-
role: 'user',
|
|
425
|
-
content: `Here is a summary of our previous conversation for context:\n\n${entry.response}`
|
|
426
|
-
})
|
|
427
|
-
messages.push({
|
|
428
|
-
role: 'assistant',
|
|
429
|
-
content: 'Understood. I will use this context as we continue.'
|
|
430
|
-
})
|
|
431
|
-
} else {
|
|
432
|
-
messages.push({
|
|
433
|
-
role: 'user',
|
|
434
|
-
content: entry.promptWithAttachments || entry.prompt
|
|
435
|
-
})
|
|
436
|
-
if (entry.response) {
|
|
437
|
-
messages.push({ role: 'assistant', content: entry.response })
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
517
|
+
// Same message list the chat turn would send (including the response of
|
|
518
|
+
// every entry), so the summary describes the conversation the model
|
|
519
|
+
// actually saw.
|
|
520
|
+
const messages = buildSessionMessages({
|
|
521
|
+
history: store.aiChatHistory,
|
|
522
|
+
chatSessionId: sessionId,
|
|
523
|
+
role: appendMandatoryGuardrails(firstEntry.roleAI + `;用[${lang}]回复`)
|
|
524
|
+
})
|
|
441
525
|
|
|
442
526
|
const summaryPrompt = 'Please summarize the above conversation concisely. Include key information, decisions, context, and any important details that would be needed to continue this conversation effectively.'
|
|
443
527
|
messages.push({ role: 'user', content: summaryPrompt })
|
|
@@ -484,8 +568,10 @@ export default Store => {
|
|
|
484
568
|
compressed: true
|
|
485
569
|
}
|
|
486
570
|
|
|
487
|
-
// Append compress entry, preserve existing history
|
|
488
|
-
|
|
571
|
+
// Append compress entry, preserve existing history. Reassign rather than
|
|
572
|
+
// push for the same reason as removeAiHistory -- the panel has to see the
|
|
573
|
+
// new context size, which is the whole point of compressing.
|
|
574
|
+
store.aiChatHistory = [...store.aiChatHistory, compressedEntry]
|
|
489
575
|
}
|
|
490
576
|
|
|
491
577
|
Store.prototype.toggleChatSessions = action(function () {
|
|
@@ -566,7 +652,9 @@ export default Store => {
|
|
|
566
652
|
}
|
|
567
653
|
|
|
568
654
|
Store.prototype.aiConfigMissing = function () {
|
|
569
|
-
return aiConfigsArr
|
|
655
|
+
return aiConfigsArr
|
|
656
|
+
.filter(k => !optionalAIConfigsArr.includes(k))
|
|
657
|
+
.some(k => !window.store.config[k])
|
|
570
658
|
}
|
|
571
659
|
|
|
572
660
|
Store.prototype.clearHistory = function () {
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
leftSidePanelWidthKey,
|
|
20
20
|
rightSidebarWidthKey,
|
|
21
21
|
rightPanelPinnedKey,
|
|
22
|
+
cmdHistoryInRightPanelKey,
|
|
23
|
+
quickCommandsInRightPanelKey,
|
|
22
24
|
addPanelWidthLsKey,
|
|
23
25
|
qmSortByFrequencyKey,
|
|
24
26
|
resolutionsLsKey,
|
|
@@ -95,6 +97,10 @@ export default () => {
|
|
|
95
97
|
_batchInputSelectedTabIds: new Set(),
|
|
96
98
|
aiChatHistory: [],
|
|
97
99
|
agentRunning: false,
|
|
100
|
+
// live context accounting while an agent loop is running: the agent
|
|
101
|
+
// carries tool schemas and tool results that the session history does
|
|
102
|
+
// not, so it publishes its own figure here (see components/ai/agent.js)
|
|
103
|
+
aiContextInfo: null,
|
|
98
104
|
currentChatSessionId: window.localStorage.getItem(lastAiChatSessionIdKey) || '',
|
|
99
105
|
showChatSessions: false,
|
|
100
106
|
|
|
@@ -127,6 +133,8 @@ export default () => {
|
|
|
127
133
|
rightPanelVisible: false,
|
|
128
134
|
rightPanelTab: 'info',
|
|
129
135
|
rightPanelPinned: ls.getItem(rightPanelPinnedKey) === 'true',
|
|
136
|
+
cmdHistoryInRightPanel: ls.getItem(cmdHistoryInRightPanelKey) === 'true',
|
|
137
|
+
quickCommandsInRightPanel: ls.getItem(quickCommandsInRightPanelKey) === 'true',
|
|
130
138
|
_rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
|
|
131
139
|
showAIConfigModal: false,
|
|
132
140
|
|
|
@@ -14,6 +14,10 @@ import { initWsCommon } from '../common/fetch-from-server'
|
|
|
14
14
|
import safeParse from '../common/parse-json-safe'
|
|
15
15
|
import initWatch from './watch'
|
|
16
16
|
import { parseQuickConnect } from '../common/parse-quick-connect'
|
|
17
|
+
import message from '../components/common/message'
|
|
18
|
+
import fs from '../common/fs'
|
|
19
|
+
import { parseVv, describeVv } from '../common/parse-vv'
|
|
20
|
+
import { vvToTab } from '../common/vv-to-tab'
|
|
17
21
|
|
|
18
22
|
function getHost (argv, opts) {
|
|
19
23
|
const arr = argv
|
|
@@ -35,6 +39,56 @@ function getHost (argv, opts) {
|
|
|
35
39
|
return {}
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Open a .vv (virt-viewer) connection file as a new Spice session.
|
|
44
|
+
*
|
|
45
|
+
* Used when electerm is handed a file from outside:
|
|
46
|
+
* electerm /path/to/console.vv
|
|
47
|
+
*
|
|
48
|
+
* This opens a session, it does not import a bookmark. For a Proxmox file that
|
|
49
|
+
* is the only shape that can work at all: the signed proxy ticket it carries is
|
|
50
|
+
* refused by the node's :3128 daemon once it is more than about 40 seconds old,
|
|
51
|
+
* so a bookmark saved from one could never connect.
|
|
52
|
+
*
|
|
53
|
+
* The parse itself is shared with the bookmark form's "Load .vv file" button --
|
|
54
|
+
* src/client/common/parse-vv.js is the single implementation.
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} store
|
|
57
|
+
* @param {string} filePath - absolute path, resolved in the main process
|
|
58
|
+
* @returns {Promise<Object|null>} the parse result, or null on failure
|
|
59
|
+
*/
|
|
60
|
+
export async function openVvFile (store, filePath) {
|
|
61
|
+
console.debug('opening .vv file', filePath)
|
|
62
|
+
let text
|
|
63
|
+
try {
|
|
64
|
+
// fs.readFile resolves the file as a utf8 string (src/app/lib/fs.js)
|
|
65
|
+
text = await fs.readFile(filePath)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
message.error('cannot read ' + filePath + ': ' + err.message)
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
const parsed = parseVv(text)
|
|
71
|
+
if (!parsed.ok) {
|
|
72
|
+
message.error(describeVv(parsed) + ' (' + filePath + ')')
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
const tab = vvToTab(parsed, { filePath })
|
|
76
|
+
console.debug('.vv file -> tab', tab)
|
|
77
|
+
// A file that did not come from a person clicking through a form gets no
|
|
78
|
+
// chance to read a hint, so anything dropped from it has to be said out loud.
|
|
79
|
+
if (parsed.warnings.length) {
|
|
80
|
+
console.debug('.vv warnings', parsed.warnings)
|
|
81
|
+
}
|
|
82
|
+
if (parsed.ignored.length) {
|
|
83
|
+
message.warning(
|
|
84
|
+
parsed.ignored.length + ' option(s) in this .vv were skipped: ' +
|
|
85
|
+
parsed.ignored.map(d => d.key).join(', ')
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
store.ipcOpenTab(tab)
|
|
89
|
+
return parsed
|
|
90
|
+
}
|
|
91
|
+
|
|
38
92
|
export async function addTabFromCommandLine (store, opts) {
|
|
39
93
|
console.debug('command line params', opts)
|
|
40
94
|
if (!opts) {
|
|
@@ -52,6 +106,12 @@ export async function addTabFromCommandLine (store, opts) {
|
|
|
52
106
|
if (isHelp) {
|
|
53
107
|
return store.openAbout(infoTabs.cmd)
|
|
54
108
|
}
|
|
109
|
+
// A .vv connection file, e.g. `electerm /path/to/console.vv`.
|
|
110
|
+
// Checked before the protocol-URL scan below because the two cannot overlap:
|
|
111
|
+
// a path ending in .vv never matches a `scheme://` prefix.
|
|
112
|
+
if (opts.vvFile) {
|
|
113
|
+
return openVvFile(store, opts.vvFile)
|
|
114
|
+
}
|
|
55
115
|
// Check if argv contains a protocol URL (e.g., ssh://user@host)
|
|
56
116
|
// and use parseQuickConnect for proper parsing
|
|
57
117
|
if (argv && argv.length) {
|
|
@@ -297,6 +357,18 @@ export default (Store) => {
|
|
|
297
357
|
window.store.ipcOpenTab(pending)
|
|
298
358
|
}
|
|
299
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* A .vv file that arrived before the window existed -- macOS delivers
|
|
362
|
+
* 'open-file' during launch, so a cold double-click always lands here. The
|
|
363
|
+
* payload is the same shape initCommandLine() returns, so it goes through the
|
|
364
|
+
* same entry point as `electerm /path/to/console.vv`.
|
|
365
|
+
*/
|
|
366
|
+
Store.prototype.checkPendingVvFile = async function () {
|
|
367
|
+
const pending = await window.pre.runGlobalAsync('getPendingVvFile')
|
|
368
|
+
if (pending) {
|
|
369
|
+
addTabFromCommandLine(window.store, pending)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
300
372
|
Store.prototype.parseQuickConnect = function (url) {
|
|
301
373
|
return parseQuickConnect(url)
|
|
302
374
|
}
|
package/client/store/store.js
CHANGED
|
@@ -145,6 +145,12 @@ class Store {
|
|
|
145
145
|
if (rightPanelTab === 'ai') {
|
|
146
146
|
return getBrand(baseURLAI).brand || 'Custom AI Model'
|
|
147
147
|
}
|
|
148
|
+
if (rightPanelTab === 'cmdHistory') {
|
|
149
|
+
return window.translate('cmdHistory')
|
|
150
|
+
}
|
|
151
|
+
if (rightPanelTab === 'quickCommands') {
|
|
152
|
+
return window.translate('quickCommands')
|
|
153
|
+
}
|
|
148
154
|
return createTitle(window.store.currentTab)
|
|
149
155
|
}
|
|
150
156
|
|
package/client/store/watch.js
CHANGED
|
@@ -151,7 +151,9 @@ export default store => {
|
|
|
151
151
|
window.pre.runGlobalAsync('setTitle', title)
|
|
152
152
|
window.store.currentLayoutBatch = tab.batch
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
// refreshing the info panel on tab switch only concerns the info tab: the
|
|
155
|
+
// right panel can also be showing the AI chat or the cmd history
|
|
156
|
+
if (tab && store.rightPanelVisible && store.rightPanelTab === 'info') {
|
|
155
157
|
window.store.openInfoPanelAction()
|
|
156
158
|
}
|
|
157
159
|
return store.activeTabId
|