@electerm/electerm-react 5.5.35 → 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/client/common/bookmark-schemas.js +1 -0
- package/client/common/constants.js +1 -0
- package/client/common/parse-quick-connect.js +1 -0
- package/client/common/parse-widget-log.js +57 -0
- package/client/components/ai/agent-tool-call-card.jsx +7 -3
- package/client/components/ai/agent.js +176 -33
- package/client/components/ai/ai-auto-compress.js +119 -0
- package/client/components/ai/ai-chat-history-item.jsx +125 -40
- package/client/components/ai/ai-chat.jsx +67 -19
- package/client/components/ai/ai-compress.js +95 -0
- package/client/components/ai/ai-config.jsx +29 -16
- package/client/components/ai/ai-config.styl +20 -0
- package/client/components/ai/ai-context.js +1 -1
- package/client/components/ai/ai-output.jsx +1 -6
- package/client/components/ai/ai-presets.js +0 -15
- 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/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/main/main.jsx +1 -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/tab-widgets.jsx +27 -7
- package/client/components/sftp/file-item.jsx +0 -13
- package/client/components/terminal/mixins/term-touch.js +18 -0
- 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 +81 -23
- package/client/store/init-state.js +7 -0
- package/client/store/load-data.js +1 -0
- package/client/store/widgets.js +23 -5
- package/package.json +1 -1
- package/client/components/file-transfer/zip.js +0 -42
package/client/store/common.js
CHANGED
|
@@ -6,6 +6,7 @@ import handleError from '../common/error-handler'
|
|
|
6
6
|
import Modal from '../components/common/modal'
|
|
7
7
|
import { appendMandatoryGuardrails } from '../components/ai/ai-guardrails'
|
|
8
8
|
import { buildSessionMessages } from '../components/ai/ai-context'
|
|
9
|
+
import { COMPRESS_SUMMARY_PROMPT } from '../components/ai/ai-auto-compress'
|
|
9
10
|
import { debounce, some, get, pickBy } from 'lodash-es'
|
|
10
11
|
import {
|
|
11
12
|
leftSidePanelWidthKey,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
addPanelWidthLsKey,
|
|
18
19
|
connectionMap,
|
|
19
20
|
lastAiChatSessionIdKey,
|
|
21
|
+
aiAutoCompressLsKey,
|
|
20
22
|
mobileBreakpoint,
|
|
21
23
|
splitMap,
|
|
22
24
|
settingAiId,
|
|
@@ -444,6 +446,23 @@ export default Store => {
|
|
|
444
446
|
})
|
|
445
447
|
}
|
|
446
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
|
+
|
|
447
466
|
Store.prototype.removeAiHistory = function (id) {
|
|
448
467
|
const { store } = window
|
|
449
468
|
const index = store.aiChatHistory.findIndex(d => d.id === id)
|
|
@@ -486,9 +505,15 @@ export default Store => {
|
|
|
486
505
|
store.startNewChat()
|
|
487
506
|
})
|
|
488
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
|
+
|
|
489
514
|
Store.prototype.compressChatSession = async function (sessionId) {
|
|
490
515
|
const { store } = window
|
|
491
|
-
if (!sessionId) {
|
|
516
|
+
if (!sessionId || compressingSessions.has(sessionId)) {
|
|
492
517
|
return
|
|
493
518
|
}
|
|
494
519
|
const sessionEntries = store.aiChatHistory
|
|
@@ -523,32 +548,18 @@ export default Store => {
|
|
|
523
548
|
role: appendMandatoryGuardrails(firstEntry.roleAI + `;用[${lang}]回复`)
|
|
524
549
|
})
|
|
525
550
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const aiResponse = await window.pre.runGlobalAsync(
|
|
530
|
-
'AIchat',
|
|
531
|
-
summaryPrompt,
|
|
532
|
-
firstEntry.modelAI,
|
|
533
|
-
firstEntry.roleAI,
|
|
534
|
-
firstEntry.baseURLAI,
|
|
535
|
-
firstEntry.apiPathAI,
|
|
536
|
-
firstEntry.apiKeyAI,
|
|
537
|
-
firstEntry.proxyAI,
|
|
538
|
-
false,
|
|
539
|
-
firstEntry.authHeaderNameAI,
|
|
540
|
-
messages
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
if (aiResponse && aiResponse.error) {
|
|
544
|
-
return store.onError(new Error(aiResponse.error))
|
|
545
|
-
}
|
|
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 })
|
|
546
554
|
|
|
547
|
-
|
|
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".
|
|
548
559
|
const compressedEntry = {
|
|
549
560
|
id: uid(),
|
|
550
561
|
prompt: '*Compressed session summary*',
|
|
551
|
-
response:
|
|
562
|
+
response: '*Summarizing the session…*',
|
|
552
563
|
isStreaming: false,
|
|
553
564
|
pending: false,
|
|
554
565
|
sessionId: null,
|
|
@@ -572,8 +583,55 @@ export default Store => {
|
|
|
572
583
|
// push for the same reason as removeAiHistory -- the panel has to see the
|
|
573
584
|
// new context size, which is the whole point of compressing.
|
|
574
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
|
+
}
|
|
575
623
|
}
|
|
576
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
|
+
|
|
577
635
|
Store.prototype.toggleChatSessions = action(function () {
|
|
578
636
|
const { store } = window
|
|
579
637
|
store.showChatSessions = !store.showChatSessions
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
syncServerDataKey,
|
|
28
28
|
splitMap,
|
|
29
29
|
lastAiChatSessionIdKey,
|
|
30
|
+
aiAutoCompressLsKey,
|
|
30
31
|
mobileBreakpoint
|
|
31
32
|
} from '../common/constants'
|
|
32
33
|
import * as ls from '../common/safe-local-storage'
|
|
@@ -97,6 +98,10 @@ export default () => {
|
|
|
97
98
|
_batchInputSelectedTabIds: new Set(),
|
|
98
99
|
aiChatHistory: [],
|
|
99
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',
|
|
100
105
|
// live context accounting while an agent loop is running: the agent
|
|
101
106
|
// carries tool schemas and tool results that the session history does
|
|
102
107
|
// not, so it publishes its own figure here (see components/ai/agent.js)
|
|
@@ -220,6 +225,8 @@ export default () => {
|
|
|
220
225
|
exePath: '',
|
|
221
226
|
isPortable: false,
|
|
222
227
|
installSrc: '',
|
|
228
|
+
// where running widgets write their logs, from the main process
|
|
229
|
+
widgetLogPath: '',
|
|
223
230
|
showSshConfigModal: false,
|
|
224
231
|
|
|
225
232
|
// batch inputs
|
|
@@ -276,6 +276,7 @@ export default (Store) => {
|
|
|
276
276
|
store.appPath = globs.appPath
|
|
277
277
|
store.exePath = globs.exePath
|
|
278
278
|
store.isPortable = globs.isPortable
|
|
279
|
+
store.widgetLogPath = globs.widgetLogPath || ''
|
|
279
280
|
store._config = globs.config
|
|
280
281
|
window.et.langs = globs.langs
|
|
281
282
|
store.zoom(store.config.zoom, false, true)
|
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)
|
package/package.json
CHANGED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* zip/unzip remote files
|
|
3
|
-
* should only support linux server
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { runCmd } from '../terminal/terminal-apis'
|
|
7
|
-
import { getFolderFromFilePath } from '../sftp/file-read'
|
|
8
|
-
import resolve from '../../common/resolve'
|
|
9
|
-
import generate from '../../common/uid'
|
|
10
|
-
|
|
11
|
-
const isRemote = true
|
|
12
|
-
const temp = '/tmp'
|
|
13
|
-
|
|
14
|
-
export async function zipCmd (pid, filePath) {
|
|
15
|
-
// tar -czf bin.tar bin
|
|
16
|
-
const id = generate()
|
|
17
|
-
const { path, name } = getFolderFromFilePath(filePath, isRemote)
|
|
18
|
-
const np = resolve(temp, `electerm-${id}.tar`)
|
|
19
|
-
const cmd = `tar -C "${path}" -cf "${np}" "${name}"`
|
|
20
|
-
await runCmd(pid, cmd)
|
|
21
|
-
return np
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function unzipCmd (pid, from, to) {
|
|
25
|
-
const cmd = `tar -xf "${from}" -C "${to}"`
|
|
26
|
-
return runCmd(pid, cmd)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function rmCmd (pid, path) {
|
|
30
|
-
const cmd = `rm -rf "${path}"`
|
|
31
|
-
return runCmd(pid, cmd)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function mvCmd (pid, from, to) {
|
|
35
|
-
const cmd = `mv "${from}" "${to}"`
|
|
36
|
-
return runCmd(pid, cmd)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function mkdirCmd (pid, p) {
|
|
40
|
-
const cmd = `mkdir "${p}"`
|
|
41
|
-
return runCmd(pid, cmd)
|
|
42
|
-
}
|