@electerm/electerm-react 5.5.25 → 5.5.56
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 +4 -0
- package/client/common/constants.js +3 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +2 -0
- package/client/common/parse-quick-connect.js +1 -0
- package/client/common/parse-vv.js +119 -25
- package/client/common/parse-widget-log.js +57 -0
- package/client/common/vv-to-tab.js +120 -0
- package/client/components/ai/agent-tool-call-card.jsx +26 -7
- package/client/components/ai/agent.js +205 -45
- package/client/components/ai/ai-auto-compress.js +119 -0
- package/client/components/ai/ai-chat-history-item.jsx +133 -84
- package/client/components/ai/ai-chat.jsx +135 -30
- package/client/components/ai/ai-compress.js +95 -0
- package/client/components/ai/ai-config-props.js +12 -1
- package/client/components/ai/ai-config.jsx +43 -16
- package/client/components/ai/ai-config.styl +20 -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 +5 -6
- package/client/components/ai/ai-presets.js +1 -16
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/batch-op/batch-op-runner.jsx +1 -0
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/common/ssh-agent.jsx +31 -21
- 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/bookmark-form/config/ssh.js +1 -0
- package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
- package/client/components/file-transfer/transfer.jsx +3 -119
- 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 +15 -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/session/session-control.jsx +35 -2
- package/client/components/session/session-control.styl +5 -0
- package/client/components/session/session.jsx +13 -0
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/setting-panel/tab-widgets.jsx +27 -7
- package/client/components/sftp/file-item.jsx +0 -13
- 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/terminal/mixins/term-touch.js +18 -0
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/components/widgets/widget-control.jsx +4 -2
- package/client/components/widgets/widget-instance-detail.jsx +370 -0
- package/client/components/widgets/widget-instance.jsx +30 -13
- package/client/components/widgets/widget-instances.jsx +3 -1
- package/client/components/widgets/widgets-list.jsx +8 -0
- package/client/components/widgets/widgets.styl +100 -0
- package/client/store/common.js +200 -54
- package/client/store/init-state.js +15 -0
- package/client/store/load-data.js +73 -0
- package/client/store/store.js +6 -0
- package/client/store/watch.js +3 -1
- package/client/store/widgets.js +23 -5
- package/package.json +1 -1
- package/client/components/file-transfer/zip.js +0 -42
|
@@ -138,10 +138,18 @@ export default auto(function WidgetsList ({ activeItemId, store, onClickItem })
|
|
|
138
138
|
)
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// clicking a running instance shows its detail + log in the right column
|
|
142
|
+
// (the widgets list above drills into the run form instead)
|
|
143
|
+
const onClickWidgetInstance = (instance) => {
|
|
144
|
+
window.store.selectWidgetInstance(instance)
|
|
145
|
+
}
|
|
146
|
+
|
|
141
147
|
const renderInstancesSection = () => {
|
|
142
148
|
return (
|
|
143
149
|
<WidgetInstances
|
|
144
150
|
widgetInstances={widgetInstances}
|
|
151
|
+
activeItemId={activeItemId}
|
|
152
|
+
onClickItem={onClickWidgetInstance}
|
|
145
153
|
/>
|
|
146
154
|
)
|
|
147
155
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
// hover popover on a running-instance row. A long path makes it wider than the
|
|
3
|
+
// list column, and it then spills over the detail panel's heading, so it is
|
|
4
|
+
// held to the column and wrapped instead.
|
|
5
|
+
.wil-pop-info
|
|
6
|
+
max-width 220px
|
|
7
|
+
word-break break-all
|
|
8
|
+
|
|
9
|
+
// widget manager: instance detail view (widget-instance-detail.jsx)
|
|
10
|
+
.widget-instance-detail
|
|
11
|
+
display flex
|
|
12
|
+
flex-direction column
|
|
13
|
+
min-height 0
|
|
14
|
+
height 100%
|
|
15
|
+
.wil-head
|
|
16
|
+
display flex
|
|
17
|
+
align-items center
|
|
18
|
+
justify-content space-between
|
|
19
|
+
gap 8px
|
|
20
|
+
margin-bottom 10px
|
|
21
|
+
flex-shrink 0
|
|
22
|
+
.wil-head-title
|
|
23
|
+
min-width 0
|
|
24
|
+
font-size 14px
|
|
25
|
+
.wil-descriptions
|
|
26
|
+
// the log is the point of this panel, so a long config list gives up room
|
|
27
|
+
// (and scrolls) instead of squeezing it: shrinkable, and allowed to go below
|
|
28
|
+
// its content height, with the pane holding a floor of its own
|
|
29
|
+
flex 0 1 auto
|
|
30
|
+
min-height 0
|
|
31
|
+
overflow auto
|
|
32
|
+
// long values (paths, URLs, keys) must wrap instead of widening the panel
|
|
33
|
+
.ant-descriptions-item-label
|
|
34
|
+
width 90px
|
|
35
|
+
white-space nowrap
|
|
36
|
+
.ant-descriptions-item-content
|
|
37
|
+
word-break break-all
|
|
38
|
+
.wil-mono
|
|
39
|
+
font-family monospace
|
|
40
|
+
font-size 12px
|
|
41
|
+
.wil-switch
|
|
42
|
+
margin 10px 0 6px
|
|
43
|
+
flex-shrink 0
|
|
44
|
+
.wil-pane
|
|
45
|
+
flex 1
|
|
46
|
+
// a floor, so the log stays readable no matter how long the config list is
|
|
47
|
+
min-height 200px
|
|
48
|
+
overflow auto
|
|
49
|
+
padding 6px 8px
|
|
50
|
+
background var(--main-dark)
|
|
51
|
+
border 1px solid var(--main-light)
|
|
52
|
+
border-radius 4px
|
|
53
|
+
font-family monospace
|
|
54
|
+
font-size 12px
|
|
55
|
+
line-height 1.6
|
|
56
|
+
.wil-line
|
|
57
|
+
display flex
|
|
58
|
+
gap 6px
|
|
59
|
+
white-space pre-wrap
|
|
60
|
+
word-break break-word
|
|
61
|
+
border-bottom 1px solid var(--main-light)
|
|
62
|
+
&:last-child
|
|
63
|
+
border-bottom 0
|
|
64
|
+
.wil-time
|
|
65
|
+
flex-shrink 0
|
|
66
|
+
color var(--text-disabled)
|
|
67
|
+
.wil-mark
|
|
68
|
+
.wil-type
|
|
69
|
+
flex-shrink 0
|
|
70
|
+
.wil-from
|
|
71
|
+
flex-shrink 0
|
|
72
|
+
color var(--text-dark)
|
|
73
|
+
.wil-msg
|
|
74
|
+
min-width 0
|
|
75
|
+
.wil-debug
|
|
76
|
+
color var(--text-dark)
|
|
77
|
+
.wil-log
|
|
78
|
+
color var(--text)
|
|
79
|
+
.wil-warn
|
|
80
|
+
color var(--warn)
|
|
81
|
+
.wil-error
|
|
82
|
+
color var(--error)
|
|
83
|
+
.wil-ok
|
|
84
|
+
color var(--success)
|
|
85
|
+
.wil-mark
|
|
86
|
+
color var(--success)
|
|
87
|
+
.wil-fail
|
|
88
|
+
color var(--error)
|
|
89
|
+
.wil-mark
|
|
90
|
+
color var(--error)
|
|
91
|
+
.wil-info
|
|
92
|
+
color var(--text-dark)
|
|
93
|
+
.wil-empty
|
|
94
|
+
color var(--text-dark)
|
|
95
|
+
padding 6px 0
|
|
96
|
+
.wil-path
|
|
97
|
+
flex-shrink 0
|
|
98
|
+
margin-top 6px
|
|
99
|
+
color var(--text-disabled)
|
|
100
|
+
font-size 11px
|
package/client/store/common.js
CHANGED
|
@@ -5,15 +5,20 @@
|
|
|
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'
|
|
9
|
+
import { COMPRESS_SUMMARY_PROMPT } from '../components/ai/ai-auto-compress'
|
|
8
10
|
import { debounce, some, get, pickBy } from 'lodash-es'
|
|
9
11
|
import {
|
|
10
12
|
leftSidePanelWidthKey,
|
|
11
13
|
leftSideBarOpenKey,
|
|
12
14
|
rightSidebarWidthKey,
|
|
13
15
|
rightPanelPinnedKey,
|
|
16
|
+
cmdHistoryInRightPanelKey,
|
|
17
|
+
quickCommandsInRightPanelKey,
|
|
14
18
|
addPanelWidthLsKey,
|
|
15
19
|
connectionMap,
|
|
16
20
|
lastAiChatSessionIdKey,
|
|
21
|
+
aiAutoCompressLsKey,
|
|
17
22
|
mobileBreakpoint,
|
|
18
23
|
splitMap,
|
|
19
24
|
settingAiId,
|
|
@@ -25,7 +30,7 @@ import { requireTermOfUse } from '../common/term-of-use'
|
|
|
25
30
|
import { action } from 'manate'
|
|
26
31
|
import uid from '../common/uid'
|
|
27
32
|
import deepCopy from 'json-deep-copy'
|
|
28
|
-
import { aiConfigsArr } from '../components/ai/ai-config-props'
|
|
33
|
+
import { aiConfigsArr, optionalAIConfigsArr } from '../components/ai/ai-config-props'
|
|
29
34
|
|
|
30
35
|
const e = window.translate
|
|
31
36
|
const { assign } = Object
|
|
@@ -179,6 +184,100 @@ export default Store => {
|
|
|
179
184
|
ls.setItem(rightPanelPinnedKey, v + '')
|
|
180
185
|
window.store.rightPanelPinned = v
|
|
181
186
|
}
|
|
187
|
+
|
|
188
|
+
// The cmd history panel has two homes: the footer popover (the default) and
|
|
189
|
+
// the right side panel. Which one it is in is a durable preference, persisted
|
|
190
|
+
// like the right panel pin — the footer trigger has to keep landing where the
|
|
191
|
+
// user last put the panel, or the choice would only survive until the next
|
|
192
|
+
// reload.
|
|
193
|
+
Store.prototype.setCmdHistoryInRightPanel = function (v) {
|
|
194
|
+
ls.setItem(cmdHistoryInRightPanelKey, v + '')
|
|
195
|
+
window.store.cmdHistoryInRightPanel = v
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Store.prototype.openCmdHistoryPanel = function () {
|
|
199
|
+
const { store } = window
|
|
200
|
+
store.rightPanelVisible = true
|
|
201
|
+
store.rightPanelTab = 'cmdHistory'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Same toggle contract as toggleInfoPanel/toggleAIPanel: while the history
|
|
205
|
+
// lives in the right panel, the footer trigger opens and closes it.
|
|
206
|
+
Store.prototype.toggleCmdHistoryPanel = function () {
|
|
207
|
+
const { store } = window
|
|
208
|
+
if (store.rightPanelVisible && store.rightPanelTab === 'cmdHistory') {
|
|
209
|
+
store.rightPanelVisible = false
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
store.openCmdHistoryPanel()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Store.prototype.moveCmdHistoryToRightPanel = function () {
|
|
216
|
+
const { store } = window
|
|
217
|
+
store.setCmdHistoryInRightPanel(true)
|
|
218
|
+
store.openCmdHistoryPanel()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Hand the panel back to the footer popover and open it there: the move has
|
|
222
|
+
// to be visible, otherwise the panel just looks like it vanished.
|
|
223
|
+
Store.prototype.moveCmdHistoryToFooter = function () {
|
|
224
|
+
const { store } = window
|
|
225
|
+
store.setCmdHistoryInRightPanel(false)
|
|
226
|
+
if (store.rightPanelTab === 'cmdHistory') {
|
|
227
|
+
store.rightPanelVisible = false
|
|
228
|
+
}
|
|
229
|
+
refsStatic.get('CmdHistory')?.openPopover()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// The quick command panel has the same two homes as the cmd history one, and
|
|
233
|
+
// the same durable preference: the footer popup (the default) and the right
|
|
234
|
+
// side panel. The footer popup is not a popover but a floating box driven by
|
|
235
|
+
// store.openQuickCommandBar, so "docked" is expressed by the preference alone
|
|
236
|
+
// and the box itself decides which of its two forms to render.
|
|
237
|
+
Store.prototype.setQuickCommandsInRightPanel = function (v) {
|
|
238
|
+
ls.setItem(quickCommandsInRightPanelKey, v + '')
|
|
239
|
+
window.store.quickCommandsInRightPanel = v
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Store.prototype.openQuickCommandsPanel = function () {
|
|
243
|
+
const { store } = window
|
|
244
|
+
store.rightPanelVisible = true
|
|
245
|
+
store.rightPanelTab = 'quickCommands'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Same toggle contract as toggleInfoPanel/toggleAIPanel/toggleCmdHistoryPanel:
|
|
249
|
+
// while the panel lives in the right panel, the footer Q opens and closes it.
|
|
250
|
+
Store.prototype.toggleQuickCommandsPanel = function () {
|
|
251
|
+
const { store } = window
|
|
252
|
+
if (store.rightPanelVisible && store.rightPanelTab === 'quickCommands') {
|
|
253
|
+
store.rightPanelVisible = false
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
store.openQuickCommandsPanel()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
Store.prototype.moveQuickCommandsToRightPanel = function () {
|
|
260
|
+
const { store } = window
|
|
261
|
+
store.setQuickCommandsInRightPanel(true)
|
|
262
|
+
// the footer box would otherwise be left floating over a terminal it no
|
|
263
|
+
// longer owns; the pin is deliberately kept, so handing the panel back
|
|
264
|
+
// restores the exact shape it had before
|
|
265
|
+
store.openQuickCommandBar = false
|
|
266
|
+
store.openQuickCommandsPanel()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Hand the panel back to the footer and open it there: the move has to be
|
|
270
|
+
// visible, otherwise the panel just looks like it vanished. A pin that was
|
|
271
|
+
// left on while docked brings the box back pinned, which is what the user
|
|
272
|
+
// last asked for.
|
|
273
|
+
Store.prototype.moveQuickCommandsToFooter = function () {
|
|
274
|
+
const { store } = window
|
|
275
|
+
store.setQuickCommandsInRightPanel(false)
|
|
276
|
+
if (store.rightPanelTab === 'quickCommands') {
|
|
277
|
+
store.rightPanelVisible = false
|
|
278
|
+
}
|
|
279
|
+
store.openQuickCommandBar = true
|
|
280
|
+
}
|
|
182
281
|
Store.prototype.beforeExit = function (evt) {
|
|
183
282
|
const { confirmBeforeExit } = window.store.config
|
|
184
283
|
if (
|
|
@@ -347,13 +446,33 @@ export default Store => {
|
|
|
347
446
|
})
|
|
348
447
|
}
|
|
349
448
|
|
|
449
|
+
// Update one entry immutably. Both halves matter:
|
|
450
|
+
// - the store only notifies subscribers on a property write, so mutating the
|
|
451
|
+
// entry in place leaves the panel rendering what it already had;
|
|
452
|
+
// - the transcript is memo()'d per entry, and a memo() comparison of an entry
|
|
453
|
+
// that was mutated in place compares the object with itself, so React can
|
|
454
|
+
// never see the change and the entry stops updating.
|
|
455
|
+
Store.prototype.updateAiHistoryEntry = function (id, updates) {
|
|
456
|
+
const { aiChatHistory } = window.store
|
|
457
|
+
const index = aiChatHistory.findIndex(d => d.id === id)
|
|
458
|
+
if (index === -1) {
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
const next = [...aiChatHistory]
|
|
462
|
+
next[index] = { ...next[index], ...updates }
|
|
463
|
+
window.store.aiChatHistory = next
|
|
464
|
+
}
|
|
465
|
+
|
|
350
466
|
Store.prototype.removeAiHistory = function (id) {
|
|
351
467
|
const { store } = window
|
|
352
468
|
const index = store.aiChatHistory.findIndex(d => d.id === id)
|
|
353
469
|
if (index === -1) {
|
|
354
470
|
return
|
|
355
471
|
}
|
|
356
|
-
|
|
472
|
+
// Reassign instead of splice: the store only notifies subscribers on a
|
|
473
|
+
// property write, so an in-place mutation leaves the panel rendering the
|
|
474
|
+
// list it already had.
|
|
475
|
+
window.store.aiChatHistory = store.aiChatHistory.filter(d => d.id !== id)
|
|
357
476
|
}
|
|
358
477
|
|
|
359
478
|
Store.prototype.startNewChat = action(function () {
|
|
@@ -386,8 +505,17 @@ export default Store => {
|
|
|
386
505
|
store.startNewChat()
|
|
387
506
|
})
|
|
388
507
|
|
|
508
|
+
// One compression per session at a time. The placeholder entry is published
|
|
509
|
+
// before the summary arrives, so a second call (auto compress firing while a
|
|
510
|
+
// manual one is still waiting on the provider) would summarize the
|
|
511
|
+
// placeholder and append a second one.
|
|
512
|
+
const compressingSessions = new Set()
|
|
513
|
+
|
|
389
514
|
Store.prototype.compressChatSession = async function (sessionId) {
|
|
390
515
|
const { store } = window
|
|
516
|
+
if (!sessionId || compressingSessions.has(sessionId)) {
|
|
517
|
+
return
|
|
518
|
+
}
|
|
391
519
|
const sessionEntries = store.aiChatHistory
|
|
392
520
|
.filter(h => h.chatSessionId === sessionId)
|
|
393
521
|
.sort((a, b) => a.timestamp - b.timestamp)
|
|
@@ -411,60 +539,27 @@ export default Store => {
|
|
|
411
539
|
|
|
412
540
|
const firstEntry = sessionEntries[0]
|
|
413
541
|
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
|
-
}
|
|
441
|
-
|
|
442
|
-
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
|
-
messages.push({ role: 'user', content: summaryPrompt })
|
|
542
|
+
// Same message list the chat turn would send (including the response of
|
|
543
|
+
// every entry), so the summary describes the conversation the model
|
|
544
|
+
// actually saw.
|
|
545
|
+
const messages = buildSessionMessages({
|
|
546
|
+
history: store.aiChatHistory,
|
|
547
|
+
chatSessionId: sessionId,
|
|
548
|
+
role: appendMandatoryGuardrails(firstEntry.roleAI + `;用[${lang}]回复`)
|
|
549
|
+
})
|
|
444
550
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
firstEntry.modelAI,
|
|
449
|
-
firstEntry.roleAI,
|
|
450
|
-
firstEntry.baseURLAI,
|
|
451
|
-
firstEntry.apiPathAI,
|
|
452
|
-
firstEntry.apiKeyAI,
|
|
453
|
-
firstEntry.proxyAI,
|
|
454
|
-
false,
|
|
455
|
-
firstEntry.authHeaderNameAI,
|
|
456
|
-
messages
|
|
457
|
-
)
|
|
551
|
+
// The same closing request the agent loop's auto compression sends (see
|
|
552
|
+
// components/ai/ai-compress.js), so both paths summarize alike.
|
|
553
|
+
messages.push({ role: 'user', content: COMPRESS_SUMMARY_PROMPT })
|
|
458
554
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const summary = aiResponse.response || ''
|
|
555
|
+
// Show the entry before asking for the summary, and fill it in when the
|
|
556
|
+
// answer arrives. The request takes as long as any other AI request (much
|
|
557
|
+
// longer for a nearly full session), and a click that changes nothing on
|
|
558
|
+
// screen for that long reads as "compress did not work".
|
|
464
559
|
const compressedEntry = {
|
|
465
560
|
id: uid(),
|
|
466
561
|
prompt: '*Compressed session summary*',
|
|
467
|
-
response:
|
|
562
|
+
response: '*Summarizing the session…*',
|
|
468
563
|
isStreaming: false,
|
|
469
564
|
pending: false,
|
|
470
565
|
sessionId: null,
|
|
@@ -484,10 +579,59 @@ export default Store => {
|
|
|
484
579
|
compressed: true
|
|
485
580
|
}
|
|
486
581
|
|
|
487
|
-
// Append compress entry, preserve existing history
|
|
488
|
-
|
|
582
|
+
// Append compress entry, preserve existing history. Reassign rather than
|
|
583
|
+
// push for the same reason as removeAiHistory -- the panel has to see the
|
|
584
|
+
// new context size, which is the whole point of compressing.
|
|
585
|
+
store.aiChatHistory = [...store.aiChatHistory, compressedEntry]
|
|
586
|
+
compressingSessions.add(sessionId)
|
|
587
|
+
|
|
588
|
+
// Anything short of a usable summary takes the placeholder away again: an
|
|
589
|
+
// empty summary must never replace the conversation, and a half finished
|
|
590
|
+
// one must not look like a result.
|
|
591
|
+
const fail = (msg) => {
|
|
592
|
+
store.removeAiHistory(compressedEntry.id)
|
|
593
|
+
return store.onError(new Error(msg))
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
const aiResponse = await window.pre.runGlobalAsync(
|
|
598
|
+
'AIchat',
|
|
599
|
+
COMPRESS_SUMMARY_PROMPT,
|
|
600
|
+
firstEntry.modelAI,
|
|
601
|
+
firstEntry.roleAI,
|
|
602
|
+
firstEntry.baseURLAI,
|
|
603
|
+
firstEntry.apiPathAI,
|
|
604
|
+
firstEntry.apiKeyAI,
|
|
605
|
+
firstEntry.proxyAI,
|
|
606
|
+
false,
|
|
607
|
+
firstEntry.authHeaderNameAI,
|
|
608
|
+
messages
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
if (aiResponse && aiResponse.error) {
|
|
612
|
+
return fail(aiResponse.error)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const summary = (aiResponse && aiResponse.response) || ''
|
|
616
|
+
if (!summary.trim()) {
|
|
617
|
+
return fail('Compression failed: empty summary from AI')
|
|
618
|
+
}
|
|
619
|
+
store.updateAiHistoryEntry(compressedEntry.id, { response: summary })
|
|
620
|
+
} finally {
|
|
621
|
+
compressingSessions.delete(sessionId)
|
|
622
|
+
}
|
|
489
623
|
}
|
|
490
624
|
|
|
625
|
+
// Auto compression, both modes: ask turns and the agent loop read this flag
|
|
626
|
+
// before every request (see components/ai/ai-compress.js), and the panel's
|
|
627
|
+
// auto compress popover writes it. Persisted so the choice survives a reload.
|
|
628
|
+
Store.prototype.setAiAutoCompress = action(function (val) {
|
|
629
|
+
const { store } = window
|
|
630
|
+
const on = !!val
|
|
631
|
+
store.aiAutoCompress = on
|
|
632
|
+
ls.setItem(aiAutoCompressLsKey, on ? 'true' : 'false')
|
|
633
|
+
})
|
|
634
|
+
|
|
491
635
|
Store.prototype.toggleChatSessions = action(function () {
|
|
492
636
|
const { store } = window
|
|
493
637
|
store.showChatSessions = !store.showChatSessions
|
|
@@ -566,7 +710,9 @@ export default Store => {
|
|
|
566
710
|
}
|
|
567
711
|
|
|
568
712
|
Store.prototype.aiConfigMissing = function () {
|
|
569
|
-
return aiConfigsArr
|
|
713
|
+
return aiConfigsArr
|
|
714
|
+
.filter(k => !optionalAIConfigsArr.includes(k))
|
|
715
|
+
.some(k => !window.store.config[k])
|
|
570
716
|
}
|
|
571
717
|
|
|
572
718
|
Store.prototype.clearHistory = function () {
|
|
@@ -19,12 +19,15 @@ import {
|
|
|
19
19
|
leftSidePanelWidthKey,
|
|
20
20
|
rightSidebarWidthKey,
|
|
21
21
|
rightPanelPinnedKey,
|
|
22
|
+
cmdHistoryInRightPanelKey,
|
|
23
|
+
quickCommandsInRightPanelKey,
|
|
22
24
|
addPanelWidthLsKey,
|
|
23
25
|
qmSortByFrequencyKey,
|
|
24
26
|
resolutionsLsKey,
|
|
25
27
|
syncServerDataKey,
|
|
26
28
|
splitMap,
|
|
27
29
|
lastAiChatSessionIdKey,
|
|
30
|
+
aiAutoCompressLsKey,
|
|
28
31
|
mobileBreakpoint
|
|
29
32
|
} from '../common/constants'
|
|
30
33
|
import * as ls from '../common/safe-local-storage'
|
|
@@ -95,6 +98,14 @@ export default () => {
|
|
|
95
98
|
_batchInputSelectedTabIds: new Set(),
|
|
96
99
|
aiChatHistory: [],
|
|
97
100
|
agentRunning: false,
|
|
101
|
+
// ask turns and the agent loop compact their conversation when the window
|
|
102
|
+
// gets full; persisted here so the toggle survives a reload
|
|
103
|
+
// (see components/ai/ai-compress.js)
|
|
104
|
+
aiAutoCompress: ls.getItem(aiAutoCompressLsKey) === 'true',
|
|
105
|
+
// live context accounting while an agent loop is running: the agent
|
|
106
|
+
// carries tool schemas and tool results that the session history does
|
|
107
|
+
// not, so it publishes its own figure here (see components/ai/agent.js)
|
|
108
|
+
aiContextInfo: null,
|
|
98
109
|
currentChatSessionId: window.localStorage.getItem(lastAiChatSessionIdKey) || '',
|
|
99
110
|
showChatSessions: false,
|
|
100
111
|
|
|
@@ -127,6 +138,8 @@ export default () => {
|
|
|
127
138
|
rightPanelVisible: false,
|
|
128
139
|
rightPanelTab: 'info',
|
|
129
140
|
rightPanelPinned: ls.getItem(rightPanelPinnedKey) === 'true',
|
|
141
|
+
cmdHistoryInRightPanel: ls.getItem(cmdHistoryInRightPanelKey) === 'true',
|
|
142
|
+
quickCommandsInRightPanel: ls.getItem(quickCommandsInRightPanelKey) === 'true',
|
|
130
143
|
_rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
|
|
131
144
|
showAIConfigModal: false,
|
|
132
145
|
|
|
@@ -212,6 +225,8 @@ export default () => {
|
|
|
212
225
|
exePath: '',
|
|
213
226
|
isPortable: false,
|
|
214
227
|
installSrc: '',
|
|
228
|
+
// where running widgets write their logs, from the main process
|
|
229
|
+
widgetLogPath: '',
|
|
215
230
|
showSshConfigModal: false,
|
|
216
231
|
|
|
217
232
|
// batch inputs
|
|
@@ -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) {
|
|
@@ -216,6 +276,7 @@ export default (Store) => {
|
|
|
216
276
|
store.appPath = globs.appPath
|
|
217
277
|
store.exePath = globs.exePath
|
|
218
278
|
store.isPortable = globs.isPortable
|
|
279
|
+
store.widgetLogPath = globs.widgetLogPath || ''
|
|
219
280
|
store._config = globs.config
|
|
220
281
|
window.et.langs = globs.langs
|
|
221
282
|
store.zoom(store.config.zoom, false, true)
|
|
@@ -297,6 +358,18 @@ export default (Store) => {
|
|
|
297
358
|
window.store.ipcOpenTab(pending)
|
|
298
359
|
}
|
|
299
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* A .vv file that arrived before the window existed -- macOS delivers
|
|
363
|
+
* 'open-file' during launch, so a cold double-click always lands here. The
|
|
364
|
+
* payload is the same shape initCommandLine() returns, so it goes through the
|
|
365
|
+
* same entry point as `electerm /path/to/console.vv`.
|
|
366
|
+
*/
|
|
367
|
+
Store.prototype.checkPendingVvFile = async function () {
|
|
368
|
+
const pending = await window.pre.runGlobalAsync('getPendingVvFile')
|
|
369
|
+
if (pending) {
|
|
370
|
+
addTabFromCommandLine(window.store, pending)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
300
373
|
Store.prototype.parseQuickConnect = function (url) {
|
|
301
374
|
return parseQuickConnect(url)
|
|
302
375
|
}
|
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
|
package/client/store/widgets.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import message from '../components/common/message'
|
|
6
6
|
import {
|
|
7
|
-
settingMap
|
|
7
|
+
settingMap,
|
|
8
|
+
settingPanelMobileBreakpoint
|
|
8
9
|
} from '../common/constants'
|
|
9
10
|
import getInitItem from '../common/init-setting-item'
|
|
10
11
|
import deepCopy from 'json-deep-copy'
|
|
@@ -27,12 +28,28 @@ export default Store => {
|
|
|
27
28
|
const {
|
|
28
29
|
widgetInstances
|
|
29
30
|
} = window.store
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// assigned, not spliced: an in-place mutation fires no subscriber, and the
|
|
32
|
+
// instance list, the sidebar badge and the detail panel all read this array
|
|
33
|
+
window.store.widgetInstances = widgetInstances.filter(w => w.id !== instanceId)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Selecting a running instance in the widgets list shows its detail + log in
|
|
37
|
+
// the right column, the same way selecting a widget shows its run form.
|
|
38
|
+
Store.prototype.selectWidgetInstance = (instance) => {
|
|
39
|
+
const { store } = window
|
|
40
|
+
store.setSettingItem(instance)
|
|
41
|
+
// narrow panels drill into the right col content after picking an instance
|
|
42
|
+
if (window.store.innerWidth <= settingPanelMobileBreakpoint) {
|
|
43
|
+
window.store.settingMobileView = 'content'
|
|
33
44
|
}
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
// back to the widget form — used when the instance being viewed is gone
|
|
48
|
+
Store.prototype.closeWidgetInstance = () => {
|
|
49
|
+
const { store } = window
|
|
50
|
+
store.setSettingItem(getInitItem([], settingMap.widgets))
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
Store.prototype.stopWidget = async (instanceId) => {
|
|
37
54
|
const {
|
|
38
55
|
store
|
|
@@ -102,10 +119,11 @@ export default Store => {
|
|
|
102
119
|
widgetId: result.widgetId,
|
|
103
120
|
serverInfo: result.serverInfo,
|
|
104
121
|
config: item.config,
|
|
122
|
+
startedAt: Date.now(),
|
|
105
123
|
autoRun: true,
|
|
106
124
|
autoRunId: item.id
|
|
107
125
|
}
|
|
108
|
-
store.widgetInstances.
|
|
126
|
+
store.widgetInstances = [...store.widgetInstances, instance]
|
|
109
127
|
}
|
|
110
128
|
} catch (err) {
|
|
111
129
|
console.error(`Failed to autorun widget ${item.widgetId}:`, err)
|