@electerm/electerm-react 5.5.15 → 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 +14 -4
- package/client/common/bookmark-schemas.js +3 -0
- package/client/common/constants.js +16 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +4 -2
- package/client/common/parse-vv.js +407 -0
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- 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 +13 -2
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/bg/css-overwrite.jsx +16 -1
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +138 -0
- package/client/components/bookmark-form/config/spice.js +22 -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 +46 -15
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +12 -13
- package/client/components/main/main.jsx +23 -3
- package/client/components/main/term-fullscreen.styl +7 -0
- package/client/components/quick-commands/qm.styl +58 -0
- package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
- package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
- package/client/components/quick-commands/quick-commands-box.jsx +197 -82
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/quick-commands/quick-commands-select.jsx +11 -0
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +56 -13
- package/client/components/sidebar/bookmark-select.jsx +17 -3
- package/client/components/sidebar/history-item.jsx +20 -4
- package/client/components/sidebar/history.jsx +7 -0
- package/client/components/sidebar/index.jsx +13 -4
- package/client/components/sidebar/side-panel.jsx +8 -3
- package/client/components/sidebar/sidebar-panel.jsx +20 -7
- package/client/components/terminal/shortcut-bar.styl +14 -0
- package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
- package/client/components/terminal-info/terminal-info.styl +3 -0
- package/client/components/theme/terminal-theme-list.styl +3 -0
- package/client/components/theme/theme-ai-editor.jsx +2 -2
- package/client/components/theme/theme-form.jsx +2 -2
- package/client/components/theme/theme-list-item.jsx +59 -25
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/css/mobile.styl +28 -0
- package/client/store/common.js +164 -32
- package/client/store/init-state.js +10 -1
- package/client/store/load-data.js +72 -0
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +15 -2
- package/client/store/watch.js +3 -1
- package/package.json +1 -1
|
@@ -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/css/mobile.styl
CHANGED
|
@@ -11,6 +11,34 @@
|
|
|
11
11
|
.remote-monitor-bar
|
|
12
12
|
position fixed
|
|
13
13
|
|
|
14
|
+
// Side panels are full-screen drawers on mobile: they start at the viewport
|
|
15
|
+
// edge (the left one covers the icon bar too, so no strip of terminal is left
|
|
16
|
+
// showing) and run to the bottom edge — footer included. On desktop the
|
|
17
|
+
// footer is reserved *beside* the panel because it holds the panel's own
|
|
18
|
+
// trigger; on mobile the drawer is dismissed from its header (✕ / the
|
|
19
|
+
// bookmark/history tabs) instead, so there is nothing to keep clear. The
|
|
20
|
+
// inline width already comes from store.leftSidePanelWidth / rightPanelWidth,
|
|
21
|
+
// which are the viewport width here.
|
|
22
|
+
// Both are on the fixed frame (the right panel explicitly: its bottom edge
|
|
23
|
+
// must not drift from the fixed footer when the visual viewport changes), and
|
|
24
|
+
// both have to outrank the footer's 200 — it comes later in the DOM, so at an
|
|
25
|
+
// equal rank it would paint over the drawer. The left drawer lives inside
|
|
26
|
+
// .sidebar, which is its own stacking context (z-index 100), so lifting the
|
|
27
|
+
// drawer alone is not enough: the whole sidebar column has to clear the
|
|
28
|
+
// footer. The right panel is a root-level child, so its own z-index suffices.
|
|
29
|
+
// The icon bar takes no extra room from this: it is 43px wide and the footer
|
|
30
|
+
// starts at 44px.
|
|
31
|
+
.sidebar
|
|
32
|
+
z-index 250
|
|
33
|
+
.sidebar-list
|
|
34
|
+
left 0
|
|
35
|
+
bottom 0
|
|
36
|
+
z-index 250
|
|
37
|
+
.right-side-panel
|
|
38
|
+
position fixed
|
|
39
|
+
bottom 0
|
|
40
|
+
z-index 250
|
|
41
|
+
|
|
14
42
|
// ---- 1. Tabs: show only current tab + dropdown, close icon always visible
|
|
15
43
|
.tabs.mobile-tabs
|
|
16
44
|
display flex
|
package/client/store/common.js
CHANGED
|
@@ -5,11 +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,
|
|
14
|
+
rightPanelPinnedKey,
|
|
15
|
+
cmdHistoryInRightPanelKey,
|
|
16
|
+
quickCommandsInRightPanelKey,
|
|
13
17
|
addPanelWidthLsKey,
|
|
14
18
|
connectionMap,
|
|
15
19
|
lastAiChatSessionIdKey,
|
|
@@ -24,7 +28,7 @@ import { requireTermOfUse } from '../common/term-of-use'
|
|
|
24
28
|
import { action } from 'manate'
|
|
25
29
|
import uid from '../common/uid'
|
|
26
30
|
import deepCopy from 'json-deep-copy'
|
|
27
|
-
import { aiConfigsArr } from '../components/ai/ai-config-props'
|
|
31
|
+
import { aiConfigsArr, optionalAIConfigsArr } from '../components/ai/ai-config-props'
|
|
28
32
|
|
|
29
33
|
const e = window.translate
|
|
30
34
|
const { assign } = Object
|
|
@@ -42,6 +46,18 @@ export default Store => {
|
|
|
42
46
|
window.store.setConfig(ext)
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
// The footer info icon is a toggle: the same click opens and closes the
|
|
50
|
+
// panel, so the trigger never has to be hunted down in the panel header.
|
|
51
|
+
Store.prototype.toggleInfoPanel = action(function () {
|
|
52
|
+
const { store } = window
|
|
53
|
+
const isOpen = store.rightPanelVisible && store.rightPanelTab === 'info'
|
|
54
|
+
store.rightPanelVisible = !isOpen
|
|
55
|
+
store.rightPanelTab = 'info'
|
|
56
|
+
if (!isOpen) {
|
|
57
|
+
store.openInfoPanelAction()
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
45
61
|
Store.prototype.openInfoPanel = action(function () {
|
|
46
62
|
const { store } = window
|
|
47
63
|
store.rightPanelVisible = true
|
|
@@ -122,7 +138,13 @@ export default Store => {
|
|
|
122
138
|
Object.assign(window.store._termSearchOptions, update)
|
|
123
139
|
}
|
|
124
140
|
|
|
141
|
+
// Both panel widths are desktop-only preferences: on mobile the panel is a
|
|
142
|
+
// full-width drawer whose width is fixed, so a drag (or any other caller)
|
|
143
|
+
// must not overwrite the desktop value that is still in localStorage.
|
|
125
144
|
Store.prototype.setLeftSidePanelWidth = function (v) {
|
|
145
|
+
if (window.store.isMobile) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
126
148
|
ls.setItem(leftSidePanelWidthKey, v)
|
|
127
149
|
window.store._leftSidePanelWidth = v
|
|
128
150
|
}
|
|
@@ -144,9 +166,116 @@ export default Store => {
|
|
|
144
166
|
}
|
|
145
167
|
|
|
146
168
|
Store.prototype.setRightSidePanelWidth = function (v) {
|
|
169
|
+
if (window.store.isMobile) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
147
172
|
ls.setItem(rightSidebarWidthKey, v)
|
|
148
173
|
window.store._rightPanelWidth = v
|
|
149
174
|
}
|
|
175
|
+
|
|
176
|
+
// Persist the pin the same way the left sidebar does (sidebarPinnedKey):
|
|
177
|
+
// pinned is a durable layout preference, not a per-session toggle. It matters
|
|
178
|
+
// more here because pinned and unpinned are visibly different modes — the
|
|
179
|
+
// unpinned panel is an overlay that clears the footer, the pinned one is a
|
|
180
|
+
// full-height dock.
|
|
181
|
+
Store.prototype.setRightPanelPinned = function (v) {
|
|
182
|
+
ls.setItem(rightPanelPinnedKey, v + '')
|
|
183
|
+
window.store.rightPanelPinned = v
|
|
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
|
+
}
|
|
150
279
|
Store.prototype.beforeExit = function (evt) {
|
|
151
280
|
const { confirmBeforeExit } = window.store.config
|
|
152
281
|
if (
|
|
@@ -286,6 +415,18 @@ export default Store => {
|
|
|
286
415
|
}
|
|
287
416
|
}
|
|
288
417
|
|
|
418
|
+
// Same toggle contract as toggleInfoPanel: the footer AI button opens and
|
|
419
|
+
// closes its own panel. The open path must go through handleOpenAIPanel so
|
|
420
|
+
// the missing-config prompt still fires; closing must not touch config.
|
|
421
|
+
Store.prototype.toggleAIPanel = function () {
|
|
422
|
+
const { store } = window
|
|
423
|
+
if (store.rightPanelVisible && store.rightPanelTab === 'ai') {
|
|
424
|
+
store.rightPanelVisible = false
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
store.handleOpenAIPanel()
|
|
428
|
+
}
|
|
429
|
+
|
|
289
430
|
Store.prototype.explainWithAi = function (txt) {
|
|
290
431
|
const { store } = window
|
|
291
432
|
store.handleOpenAIPanel()
|
|
@@ -309,7 +450,10 @@ export default Store => {
|
|
|
309
450
|
if (index === -1) {
|
|
310
451
|
return
|
|
311
452
|
}
|
|
312
|
-
|
|
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)
|
|
313
457
|
}
|
|
314
458
|
|
|
315
459
|
Store.prototype.startNewChat = action(function () {
|
|
@@ -344,6 +488,9 @@ export default Store => {
|
|
|
344
488
|
|
|
345
489
|
Store.prototype.compressChatSession = async function (sessionId) {
|
|
346
490
|
const { store } = window
|
|
491
|
+
if (!sessionId) {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
347
494
|
const sessionEntries = store.aiChatHistory
|
|
348
495
|
.filter(h => h.chatSessionId === sessionId)
|
|
349
496
|
.sort((a, b) => a.timestamp - b.timestamp)
|
|
@@ -367,33 +514,14 @@ export default Store => {
|
|
|
367
514
|
|
|
368
515
|
const firstEntry = sessionEntries[0]
|
|
369
516
|
const lang = firstEntry.languageAI || store.getLangName()
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (entry.compressed) {
|
|
379
|
-
messages.push({
|
|
380
|
-
role: 'user',
|
|
381
|
-
content: `Here is a summary of our previous conversation for context:\n\n${entry.response}`
|
|
382
|
-
})
|
|
383
|
-
messages.push({
|
|
384
|
-
role: 'assistant',
|
|
385
|
-
content: 'Understood. I will use this context as we continue.'
|
|
386
|
-
})
|
|
387
|
-
} else {
|
|
388
|
-
messages.push({
|
|
389
|
-
role: 'user',
|
|
390
|
-
content: entry.promptWithAttachments || entry.prompt
|
|
391
|
-
})
|
|
392
|
-
if (entry.response) {
|
|
393
|
-
messages.push({ role: 'assistant', content: entry.response })
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
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
|
+
})
|
|
397
525
|
|
|
398
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.'
|
|
399
527
|
messages.push({ role: 'user', content: summaryPrompt })
|
|
@@ -440,8 +568,10 @@ export default Store => {
|
|
|
440
568
|
compressed: true
|
|
441
569
|
}
|
|
442
570
|
|
|
443
|
-
// Append compress entry, preserve existing history
|
|
444
|
-
|
|
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]
|
|
445
575
|
}
|
|
446
576
|
|
|
447
577
|
Store.prototype.toggleChatSessions = action(function () {
|
|
@@ -522,7 +652,9 @@ export default Store => {
|
|
|
522
652
|
}
|
|
523
653
|
|
|
524
654
|
Store.prototype.aiConfigMissing = function () {
|
|
525
|
-
return aiConfigsArr
|
|
655
|
+
return aiConfigsArr
|
|
656
|
+
.filter(k => !optionalAIConfigsArr.includes(k))
|
|
657
|
+
.some(k => !window.store.config[k])
|
|
526
658
|
}
|
|
527
659
|
|
|
528
660
|
Store.prototype.clearHistory = function () {
|
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
localAddrBookmarkLsKey,
|
|
19
19
|
leftSidePanelWidthKey,
|
|
20
20
|
rightSidebarWidthKey,
|
|
21
|
+
rightPanelPinnedKey,
|
|
22
|
+
cmdHistoryInRightPanelKey,
|
|
23
|
+
quickCommandsInRightPanelKey,
|
|
21
24
|
addPanelWidthLsKey,
|
|
22
25
|
qmSortByFrequencyKey,
|
|
23
26
|
resolutionsLsKey,
|
|
@@ -94,6 +97,10 @@ export default () => {
|
|
|
94
97
|
_batchInputSelectedTabIds: new Set(),
|
|
95
98
|
aiChatHistory: [],
|
|
96
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,
|
|
97
104
|
currentChatSessionId: window.localStorage.getItem(lastAiChatSessionIdKey) || '',
|
|
98
105
|
showChatSessions: false,
|
|
99
106
|
|
|
@@ -125,7 +132,9 @@ export default () => {
|
|
|
125
132
|
terminalInfoProps: {},
|
|
126
133
|
rightPanelVisible: false,
|
|
127
134
|
rightPanelTab: 'info',
|
|
128
|
-
rightPanelPinned:
|
|
135
|
+
rightPanelPinned: ls.getItem(rightPanelPinnedKey) === 'true',
|
|
136
|
+
cmdHistoryInRightPanel: ls.getItem(cmdHistoryInRightPanelKey) === 'true',
|
|
137
|
+
quickCommandsInRightPanel: ls.getItem(quickCommandsInRightPanelKey) === 'true',
|
|
129
138
|
_rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
|
|
130
139
|
showAIConfigModal: false,
|
|
131
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/sidebar.js
CHANGED
|
@@ -25,7 +25,11 @@ export default Store => {
|
|
|
25
25
|
// through this one method.
|
|
26
26
|
Store.prototype.openLeftSidePanel = function (field = 'bookmarks') {
|
|
27
27
|
const { store } = window
|
|
28
|
-
|
|
28
|
+
// A pinned panel is already open, so there is nothing to toggle — except
|
|
29
|
+
// on mobile, where pinning is not a reachable mode (no pin control) and
|
|
30
|
+
// the panel is a full-width overlay. Honouring a stale desktop pin here
|
|
31
|
+
// would leave the bookmark button dead on arrival.
|
|
32
|
+
if (store.pinned && !store.isMobile) {
|
|
29
33
|
return
|
|
30
34
|
}
|
|
31
35
|
store.setOpenedSideBar(store.openedSideBar ? '' : field)
|
package/client/store/store.js
CHANGED
|
@@ -61,10 +61,15 @@ class Store {
|
|
|
61
61
|
return window.store.innerWidth
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// Mobile turns a side panel into a full-screen drawer: it spans the whole
|
|
65
|
+
// viewport and its width is not user-adjustable. So the stored desktop width
|
|
66
|
+
// is ignored here, and so is the icon bar (the drawer covers it) — and the
|
|
67
|
+
// old 500px cap, which left a strip of terminal showing on windows between
|
|
68
|
+
// 500 and 600px, i.e. the top of the mobile range.
|
|
64
69
|
get leftSidePanelWidth () {
|
|
65
70
|
const { store } = window
|
|
66
71
|
if (store.isMobile) {
|
|
67
|
-
return
|
|
72
|
+
return store.innerWidth
|
|
68
73
|
}
|
|
69
74
|
return store._leftSidePanelWidth
|
|
70
75
|
}
|
|
@@ -78,10 +83,12 @@ class Store {
|
|
|
78
83
|
return store._leftSideBarOpen ? sidebarWidth : 0
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
// Same as leftSidePanelWidth: full-screen drawer on mobile, width not
|
|
87
|
+
// user-adjustable.
|
|
81
88
|
get rightPanelWidth () {
|
|
82
89
|
const { store } = window
|
|
83
90
|
if (store.isMobile) {
|
|
84
|
-
return
|
|
91
|
+
return store.innerWidth
|
|
85
92
|
}
|
|
86
93
|
return store._rightPanelWidth
|
|
87
94
|
}
|
|
@@ -138,6 +145,12 @@ class Store {
|
|
|
138
145
|
if (rightPanelTab === 'ai') {
|
|
139
146
|
return getBrand(baseURLAI).brand || 'Custom AI Model'
|
|
140
147
|
}
|
|
148
|
+
if (rightPanelTab === 'cmdHistory') {
|
|
149
|
+
return window.translate('cmdHistory')
|
|
150
|
+
}
|
|
151
|
+
if (rightPanelTab === 'quickCommands') {
|
|
152
|
+
return window.translate('quickCommands')
|
|
153
|
+
}
|
|
141
154
|
return createTitle(window.store.currentTab)
|
|
142
155
|
}
|
|
143
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
|