@electerm/electerm-react 3.15.50 → 3.15.58
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/components/ai/agent.js +100 -95
- package/client/components/ai/ai-chat-entry.jsx +1 -2
- package/client/components/ai/ai-chat-history-item.jsx +1 -1
- package/client/components/ai/ai-chat.jsx +12 -1
- package/client/components/ai/ai-output.jsx +14 -3
- package/client/components/ai/ai.styl +14 -0
- package/client/components/main/main.jsx +2 -1
- package/client/components/rdp/rdp-session.jsx +1 -2
- package/client/components/setting-panel/setting-modal.jsx +6 -8
- package/client/components/setting-sync/server-data-status.jsx +31 -13
- package/client/components/setting-sync/sync-data-compare.jsx +92 -0
- package/client/components/spice/spice-session.jsx +1 -2
- package/client/components/terminal/terminal-interactive-ui.jsx +1 -1
- package/client/components/terminal/xterm-loader.js +11 -13
- package/client/components/terminal-info/terminal-info-entry.jsx +1 -2
- package/client/components/text-editor/text-editor-entry.jsx +1 -2
- package/client/components/vnc/vnc-session.jsx +1 -2
- package/client/css/basic.styl +6 -1
- package/client/store/init-state.js +1 -0
- package/client/store/sidebar.js +3 -0
- package/client/store/sync.js +98 -1
- package/package.json +1 -1
- package/client/common/import-retry.js +0 -23
|
@@ -46,116 +46,121 @@ async function callBackendAIchatWithTools (messages, config) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export async function runAgentLoop (chatEntry, config, abortRef, setIsStreaming) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
49
|
+
window.store.agentRunning = true
|
|
50
|
+
try {
|
|
51
|
+
const messages = [
|
|
52
|
+
{ role: 'system', content: buildAgentSystemPrompt(config) },
|
|
53
|
+
{ role: 'user', content: chatEntry.prompt }
|
|
54
|
+
]
|
|
55
|
+
const toolCallsLog = []
|
|
56
|
+
let accumulatedContent = ''
|
|
57
|
+
|
|
58
|
+
setIsStreaming(true)
|
|
59
|
+
updateChatEntry(chatEntry, {
|
|
60
|
+
toolCalls: [],
|
|
61
|
+
response: ''
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
65
|
+
if (abortRef && abortRef.current) {
|
|
66
|
+
setIsStreaming(false)
|
|
67
|
+
updateChatEntry(chatEntry, {
|
|
68
|
+
response: accumulatedContent + '\n\n*(Agent stopped by user)*'
|
|
69
|
+
})
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
setIsStreaming(false)
|
|
75
|
-
updateChatEntry(chatEntry, {
|
|
76
|
-
response: accumulatedContent + `\n\n**Error:** ${result.error}`
|
|
77
|
-
})
|
|
78
|
-
return
|
|
79
|
-
}
|
|
73
|
+
const result = await callBackendAIchatWithTools(messages, config)
|
|
80
74
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
75
|
+
if (result.error) {
|
|
76
|
+
setIsStreaming(false)
|
|
77
|
+
updateChatEntry(chatEntry, {
|
|
78
|
+
response: accumulatedContent + `\n\n**Error:** ${result.error}`
|
|
79
|
+
})
|
|
80
|
+
return
|
|
81
|
+
}
|
|
89
82
|
|
|
90
|
-
|
|
83
|
+
const assistantMessage = result.message
|
|
84
|
+
if (!assistantMessage) {
|
|
85
|
+
setIsStreaming(false)
|
|
86
|
+
updateChatEntry(chatEntry, {
|
|
87
|
+
response: accumulatedContent || 'No response from AI.'
|
|
88
|
+
})
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
accumulatedContent += (accumulatedContent ? '\n\n' : '') + assistantMessage.content
|
|
94
|
-
updateChatEntry(chatEntry, {
|
|
95
|
-
response: accumulatedContent
|
|
96
|
-
})
|
|
97
|
-
}
|
|
92
|
+
messages.push(assistantMessage)
|
|
98
93
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
94
|
+
if (assistantMessage.content) {
|
|
95
|
+
accumulatedContent += (accumulatedContent ? '\n\n' : '') + assistantMessage.content
|
|
96
|
+
updateChatEntry(chatEntry, {
|
|
97
|
+
response: accumulatedContent
|
|
98
|
+
})
|
|
99
|
+
}
|
|
106
100
|
|
|
107
|
-
|
|
108
|
-
if (abortRef && abortRef.current) {
|
|
101
|
+
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
109
102
|
setIsStreaming(false)
|
|
110
103
|
updateChatEntry(chatEntry, {
|
|
111
|
-
response: accumulatedContent
|
|
104
|
+
response: accumulatedContent
|
|
112
105
|
})
|
|
113
106
|
return
|
|
114
107
|
}
|
|
115
108
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
109
|
+
for (const toolCall of assistantMessage.tool_calls) {
|
|
110
|
+
if (abortRef && abortRef.current) {
|
|
111
|
+
setIsStreaming(false)
|
|
112
|
+
updateChatEntry(chatEntry, {
|
|
113
|
+
response: accumulatedContent + '\n\n*(Agent stopped by user)*'
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let args
|
|
119
|
+
try {
|
|
120
|
+
args = JSON.parse(toolCall.function.arguments)
|
|
121
|
+
} catch {
|
|
122
|
+
args = {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const toolEntry = {
|
|
126
|
+
id: toolCall.id,
|
|
127
|
+
name: toolCall.function.name,
|
|
128
|
+
args,
|
|
129
|
+
status: 'running',
|
|
130
|
+
result: null
|
|
131
|
+
}
|
|
132
|
+
toolCallsLog.push(toolEntry)
|
|
133
|
+
updateChatEntry(chatEntry, {
|
|
134
|
+
toolCalls: [...toolCallsLog]
|
|
135
|
+
})
|
|
122
136
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
toolCalls: [...toolCallsLog]
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
let toolResult
|
|
136
|
-
try {
|
|
137
|
-
toolResult = await executeToolCall(toolCall.function.name, args)
|
|
138
|
-
toolEntry.status = 'completed'
|
|
139
|
-
toolEntry.result = toolResult
|
|
140
|
-
} catch (err) {
|
|
141
|
-
toolEntry.status = 'error'
|
|
142
|
-
toolEntry.result = err.message
|
|
143
|
-
}
|
|
137
|
+
let toolResult
|
|
138
|
+
try {
|
|
139
|
+
toolResult = await executeToolCall(toolCall.function.name, args)
|
|
140
|
+
toolEntry.status = 'completed'
|
|
141
|
+
toolEntry.result = toolResult
|
|
142
|
+
} catch (err) {
|
|
143
|
+
toolEntry.status = 'error'
|
|
144
|
+
toolEntry.result = err.message
|
|
145
|
+
}
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
updateChatEntry(chatEntry, {
|
|
148
|
+
toolCalls: [...toolCallsLog]
|
|
149
|
+
})
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
messages.push({
|
|
152
|
+
role: 'tool',
|
|
153
|
+
tool_call_id: toolCall.id,
|
|
154
|
+
content: toolEntry.result
|
|
155
|
+
})
|
|
156
|
+
}
|
|
154
157
|
}
|
|
155
|
-
}
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
setIsStreaming(false)
|
|
160
|
+
updateChatEntry(chatEntry, {
|
|
161
|
+
response: accumulatedContent + '\n\n*(Agent reached maximum iterations)*'
|
|
162
|
+
})
|
|
163
|
+
} finally {
|
|
164
|
+
window.store.agentRunning = false
|
|
165
|
+
}
|
|
161
166
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { lazy, Suspense } from 'react'
|
|
2
|
-
import importRetry from '../../common/import-retry'
|
|
3
2
|
|
|
4
|
-
const AIChat = lazy(() =>
|
|
3
|
+
const AIChat = lazy(() => import('./ai-chat'))
|
|
5
4
|
|
|
6
5
|
export default function AIChatEntry (props) {
|
|
7
6
|
return (
|
|
@@ -173,7 +173,6 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
173
173
|
{showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
|
174
174
|
</span>
|
|
175
175
|
<span>{prompt}</span>
|
|
176
|
-
{renderStopButton()}
|
|
177
176
|
</div>
|
|
178
177
|
),
|
|
179
178
|
type: 'info'
|
|
@@ -244,6 +243,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
244
243
|
</div>
|
|
245
244
|
{renderToolCalls()}
|
|
246
245
|
{showOutput && <AIOutput item={item} />}
|
|
246
|
+
{renderStopButton()}
|
|
247
247
|
</div>
|
|
248
248
|
)
|
|
249
249
|
}
|
|
@@ -25,6 +25,7 @@ export default function AIChat (props) {
|
|
|
25
25
|
const [prompt, setPrompt] = useState('')
|
|
26
26
|
const [mode, setMode] = useState(() => getItem(aiChatModeLsKey) || 'ask')
|
|
27
27
|
const isAgent = mode === 'agent'
|
|
28
|
+
const submitDisabled = isAgent && props.agentRunning
|
|
28
29
|
|
|
29
30
|
function handlePromptChange (e) {
|
|
30
31
|
setPrompt(e.target.value)
|
|
@@ -103,6 +104,14 @@ export default function AIChat (props) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
function renderSendIcon () {
|
|
107
|
+
if (submitDisabled) {
|
|
108
|
+
return (
|
|
109
|
+
<SendOutlined
|
|
110
|
+
className='mg1l send-to-ai-icon disabled'
|
|
111
|
+
title='Agent is running, please wait'
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
106
115
|
return (
|
|
107
116
|
<SendOutlined
|
|
108
117
|
onClick={handleSubmit}
|
|
@@ -132,7 +141,9 @@ export default function AIChat (props) {
|
|
|
132
141
|
const handleKeyPress = (e) => {
|
|
133
142
|
if (!e.shiftKey) {
|
|
134
143
|
e.preventDefault()
|
|
135
|
-
|
|
144
|
+
if (!submitDisabled) {
|
|
145
|
+
handleSubmit()
|
|
146
|
+
}
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
149
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react'
|
|
1
2
|
import ReactMarkdown from 'react-markdown'
|
|
2
3
|
import { copy } from '../../common/clipboard'
|
|
3
4
|
import Link from '../common/external-link'
|
|
@@ -8,12 +9,20 @@ import getBrand from './get-brand'
|
|
|
8
9
|
const e = window.translate
|
|
9
10
|
|
|
10
11
|
export default function AIOutput ({ item }) {
|
|
12
|
+
const outputRef = useRef(null)
|
|
11
13
|
const {
|
|
12
14
|
response,
|
|
13
15
|
baseURLAI,
|
|
14
16
|
nameAI,
|
|
15
17
|
modelAI
|
|
16
18
|
} = item
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (outputRef.current) {
|
|
22
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
|
23
|
+
}
|
|
24
|
+
}, [response])
|
|
25
|
+
|
|
17
26
|
if (!response) {
|
|
18
27
|
return null
|
|
19
28
|
}
|
|
@@ -103,9 +112,11 @@ export default function AIOutput ({ item }) {
|
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
return (
|
|
106
|
-
<div className='
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
<div className='ai-stream-output' ref={outputRef}>
|
|
116
|
+
<div className='pd1'>
|
|
117
|
+
{renderBrand()}
|
|
118
|
+
<ReactMarkdown {...mdProps} />
|
|
119
|
+
</div>
|
|
109
120
|
</div>
|
|
110
121
|
)
|
|
111
122
|
}
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
overflow-x hidden
|
|
15
15
|
|
|
16
16
|
.chat-history-item
|
|
17
|
+
position relative
|
|
18
|
+
> .ai-stop-icon-square
|
|
19
|
+
position absolute
|
|
20
|
+
bottom 8px
|
|
21
|
+
right 8px
|
|
22
|
+
z-index 10
|
|
17
23
|
.code-block
|
|
18
24
|
border 1px dashed var(--main-darker)
|
|
19
25
|
padding 5px
|
|
@@ -26,6 +32,10 @@
|
|
|
26
32
|
// .code-block-actions
|
|
27
33
|
// display block
|
|
28
34
|
|
|
35
|
+
.ai-stream-output
|
|
36
|
+
max-height 400px
|
|
37
|
+
overflow-y auto
|
|
38
|
+
|
|
29
39
|
.ai-chat-input
|
|
30
40
|
position relative
|
|
31
41
|
margin-top 10px
|
|
@@ -53,6 +63,10 @@
|
|
|
53
63
|
white-space pre-wrap
|
|
54
64
|
word-break break-all
|
|
55
65
|
|
|
66
|
+
.send-to-ai-icon.disabled
|
|
67
|
+
opacity 0.4
|
|
68
|
+
cursor not-allowed
|
|
69
|
+
|
|
56
70
|
.clear-ai-icon
|
|
57
71
|
position relative
|
|
58
72
|
&::after
|
|
@@ -232,7 +232,8 @@ export default auto(function Index (props) {
|
|
|
232
232
|
tabs: store.getTabs(),
|
|
233
233
|
activeTabId: store.activeTabId,
|
|
234
234
|
showAIConfig: store.showAIConfig,
|
|
235
|
-
rightPanelTab
|
|
235
|
+
rightPanelTab,
|
|
236
|
+
agentRunning: store.agentRunning
|
|
236
237
|
}
|
|
237
238
|
const cmdSuggestionsProps = {
|
|
238
239
|
suggestions: store.terminalCommandSuggestions
|
|
@@ -28,7 +28,6 @@ import { FileTransferManager, createFileLogger } from './file-transfer'
|
|
|
28
28
|
import { notification } from '../common/notification'
|
|
29
29
|
import message from '../common/message'
|
|
30
30
|
import ShowItem from '../common/show-item'
|
|
31
|
-
import importRetry from '../../common/import-retry'
|
|
32
31
|
import './rdp.styl'
|
|
33
32
|
|
|
34
33
|
const { Option } = Select
|
|
@@ -36,7 +35,7 @@ const { Option } = Select
|
|
|
36
35
|
async function loadWasmModule () {
|
|
37
36
|
if (window.ironRdp) return
|
|
38
37
|
console.debug('[RDP-CLIENT] Loading IronRDP WASM module...')
|
|
39
|
-
const mod = await
|
|
38
|
+
const mod = await import('ironrdp-wasm')
|
|
40
39
|
window.ironRdp = {
|
|
41
40
|
wasmInit: mod.default,
|
|
42
41
|
wasmSetup: mod.setup,
|
|
@@ -11,14 +11,12 @@ import {
|
|
|
11
11
|
settingMap,
|
|
12
12
|
modals
|
|
13
13
|
} from '../../common/constants'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const TabProfiles = lazy(() => importRetry(() => import('./tab-profiles')))
|
|
21
|
-
const TabWidgets = lazy(() => importRetry(() => import('./tab-widgets')))
|
|
14
|
+
const TabBookmarks = lazy(() => import('./tab-bookmarks'))
|
|
15
|
+
const TabQuickCommands = lazy(() => import('./tab-quick-commands'))
|
|
16
|
+
const TabSettings = lazy(() => import('./tab-settings'))
|
|
17
|
+
const TabThemes = lazy(() => import('./tab-themes'))
|
|
18
|
+
const TabProfiles = lazy(() => import('./tab-profiles'))
|
|
19
|
+
const TabWidgets = lazy(() => import('./tab-widgets'))
|
|
22
20
|
|
|
23
21
|
const Loading = () => <div style={{ padding: 20, textAlign: 'center' }}><Spin /></div>
|
|
24
22
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { syncTypes } from '../../common/constants'
|
|
2
2
|
import { useState } from 'react'
|
|
3
|
-
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
3
|
+
import { LoadingOutlined, ReloadOutlined, DiffOutlined } from '@ant-design/icons'
|
|
4
4
|
import dayjs from 'dayjs'
|
|
5
|
+
import SyncDataCompare from './sync-data-compare'
|
|
5
6
|
|
|
6
7
|
const e = window.translate
|
|
7
8
|
|
|
@@ -9,6 +10,7 @@ export default function ServerDataStatus (props) {
|
|
|
9
10
|
const { store } = window
|
|
10
11
|
const { type, status } = props
|
|
11
12
|
const [loading, setLoading] = useState(false)
|
|
13
|
+
const [showCompare, setShowCompare] = useState(false)
|
|
12
14
|
const token = store.getSyncToken(type)
|
|
13
15
|
const gistId = store.getSyncGistId(type)
|
|
14
16
|
const canSync = token && (gistId || type === 'custom' || type === 'cloud' || type === syncTypes.webdav)
|
|
@@ -20,6 +22,10 @@ export default function ServerDataStatus (props) {
|
|
|
20
22
|
setLoading(false)
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function handleCompare () {
|
|
26
|
+
setShowCompare(!showCompare)
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function renderReloadButton () {
|
|
24
30
|
if (loading) {
|
|
25
31
|
return (
|
|
@@ -27,10 +33,19 @@ export default function ServerDataStatus (props) {
|
|
|
27
33
|
)
|
|
28
34
|
}
|
|
29
35
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
<span>
|
|
37
|
+
<ReloadOutlined
|
|
38
|
+
className='pointer mg1r hover-black'
|
|
39
|
+
onClick={handleReload}
|
|
40
|
+
/>
|
|
41
|
+
<span
|
|
42
|
+
className='pointer mg2l hover-black'
|
|
43
|
+
onClick={handleCompare}
|
|
44
|
+
>
|
|
45
|
+
<DiffOutlined className='mg1r' />
|
|
46
|
+
{e('compare') || 'compare'}
|
|
47
|
+
</span>
|
|
48
|
+
</span>
|
|
34
49
|
)
|
|
35
50
|
}
|
|
36
51
|
|
|
@@ -59,14 +74,17 @@ export default function ServerDataStatus (props) {
|
|
|
59
74
|
} = status
|
|
60
75
|
|
|
61
76
|
return (
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
<div>
|
|
78
|
+
<p>
|
|
79
|
+
<span className='mg1r'>{e('syncServerDataStatus')}:</span>
|
|
80
|
+
<b className='mg1r'>{dayjs(lastSyncTime).format('YYYY-MM-DD HH:mm:ss')}</b>
|
|
81
|
+
<span className='mg1r'>{e('from')}:</span>
|
|
82
|
+
<b className='mg1r'>{deviceName}</b>
|
|
83
|
+
<b className='mg1r'>(v{electermVersion})</b>
|
|
84
|
+
{renderReloadButton()}
|
|
85
|
+
</p>
|
|
86
|
+
{showCompare && <SyncDataCompare syncType={type} />}
|
|
87
|
+
</div>
|
|
70
88
|
)
|
|
71
89
|
}
|
|
72
90
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync data comparison component
|
|
3
|
+
* Shows simple diff suggestions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from 'react'
|
|
7
|
+
import { Spin } from 'antd'
|
|
8
|
+
|
|
9
|
+
const e = window.translate
|
|
10
|
+
|
|
11
|
+
export default function SyncDataCompare (props) {
|
|
12
|
+
const { store } = window
|
|
13
|
+
const { syncType } = props
|
|
14
|
+
const [loading, setLoading] = useState(false)
|
|
15
|
+
const [comparison, setComparison] = useState(null)
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
loadComparison()
|
|
19
|
+
}, [syncType])
|
|
20
|
+
|
|
21
|
+
async function loadComparison () {
|
|
22
|
+
setLoading(true)
|
|
23
|
+
try {
|
|
24
|
+
const result = await store.previewServerDataWithCompare(syncType)
|
|
25
|
+
setComparison(result)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('Failed to load comparison:', err)
|
|
28
|
+
}
|
|
29
|
+
setLoading(false)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!comparison) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { comparison: comp } = comparison
|
|
37
|
+
|
|
38
|
+
// Filter only items with differences
|
|
39
|
+
const diffs = comp.filter(item => item.onlyLocal > 0 || item.onlyServer > 0)
|
|
40
|
+
|
|
41
|
+
if (diffs.length === 0) {
|
|
42
|
+
return (
|
|
43
|
+
<p className='mg1t sync-diff-text'>
|
|
44
|
+
{e('dataInSync') || 'Data in sync'}
|
|
45
|
+
</p>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const nameMap = {
|
|
50
|
+
bookmarks: e('bookmarks') || 'Bookmarks',
|
|
51
|
+
bookmarkGroups: 'Bookmark Groups',
|
|
52
|
+
terminalThemes: e('terminalThemes') || 'Terminal Themes',
|
|
53
|
+
quickCommands: e('quickCommands') || 'Quick Commands',
|
|
54
|
+
profiles: e('profiles') || 'Profiles',
|
|
55
|
+
addressBookmarks: e('addressBookmarks') || 'Address Bookmarks',
|
|
56
|
+
workspaces: e('workspaces') || 'Workspaces'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lines = diffs.map(item => {
|
|
60
|
+
const displayName = nameMap[item.name] || item.name
|
|
61
|
+
const localCount = item.localCount
|
|
62
|
+
const serverCount = item.serverCount
|
|
63
|
+
const diff = serverCount - localCount
|
|
64
|
+
let action = ''
|
|
65
|
+
if (diff > 0) {
|
|
66
|
+
action = e('download') || 'download'
|
|
67
|
+
} else if (diff < 0) {
|
|
68
|
+
action = e('upload') || 'upload'
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
text: `${e('remote') || 'remote'}: ${serverCount} ${displayName}, ${e('local') || 'local'}: ${localCount} ${displayName}`,
|
|
72
|
+
action
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className='sync-data-compare mg1t mg2b'>
|
|
78
|
+
<Spin spinning={loading}>
|
|
79
|
+
<div className='sync-diff-text'>
|
|
80
|
+
{lines.map((line, i) => (
|
|
81
|
+
<p key={i} className='mg0'>
|
|
82
|
+
{line.text}
|
|
83
|
+
{line.action && (
|
|
84
|
+
<span className='sync-suggest-action'> {'->'} {line.action} ?</span>
|
|
85
|
+
)}
|
|
86
|
+
</p>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</Spin>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -15,12 +15,11 @@ import {
|
|
|
15
15
|
} from 'antd'
|
|
16
16
|
import * as ls from '../../common/safe-local-storage'
|
|
17
17
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
18
|
-
import importRetry from '../../common/import-retry'
|
|
19
18
|
import './spice.styl'
|
|
20
19
|
|
|
21
20
|
async function loadSpiceModule () {
|
|
22
21
|
if (window.spiceHtml5) return
|
|
23
|
-
const mod = await
|
|
22
|
+
const mod = await import('spice-client')
|
|
24
23
|
window.spiceHtml5 = {
|
|
25
24
|
SpiceMainConn: mod.SpiceMainConn,
|
|
26
25
|
sendCtrlAltDel: mod.sendCtrlAltDel
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import importRetry from '../../common/import-retry'
|
|
2
|
-
|
|
3
1
|
window.xtermAddons = window.xtermAddons || {}
|
|
4
2
|
|
|
5
3
|
let xtermCssLoaded = false
|
|
@@ -7,76 +5,76 @@ let xtermCssLoaded = false
|
|
|
7
5
|
function loadXtermCss () {
|
|
8
6
|
if (xtermCssLoaded) return
|
|
9
7
|
xtermCssLoaded = true
|
|
10
|
-
|
|
8
|
+
import('@xterm/xterm/css/xterm.css')
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
export async function loadTerminal () {
|
|
14
12
|
if (window.xtermAddons.Terminal) return window.xtermAddons.Terminal
|
|
15
13
|
loadXtermCss()
|
|
16
|
-
const mod = await
|
|
14
|
+
const mod = await import('@xterm/xterm')
|
|
17
15
|
window.xtermAddons.Terminal = mod.Terminal
|
|
18
16
|
return window.xtermAddons.Terminal
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export async function loadFitAddon () {
|
|
22
20
|
if (window.xtermAddons.FitAddon) return window.xtermAddons.FitAddon
|
|
23
|
-
const mod = await
|
|
21
|
+
const mod = await import('@xterm/addon-fit')
|
|
24
22
|
window.xtermAddons.FitAddon = mod.FitAddon
|
|
25
23
|
return window.xtermAddons.FitAddon
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
export async function loadAttachAddon () {
|
|
29
27
|
if (window.xtermAddons.AttachAddon) return window.xtermAddons.AttachAddon
|
|
30
|
-
const mod = await
|
|
28
|
+
const mod = await import('@xterm/addon-attach')
|
|
31
29
|
window.xtermAddons.AttachAddon = mod.AttachAddon
|
|
32
30
|
return window.xtermAddons.AttachAddon
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
export async function loadWebLinksAddon () {
|
|
36
34
|
if (window.xtermAddons.WebLinksAddon) return window.xtermAddons.WebLinksAddon
|
|
37
|
-
const mod = await
|
|
35
|
+
const mod = await import('@xterm/addon-web-links')
|
|
38
36
|
window.xtermAddons.WebLinksAddon = mod.WebLinksAddon
|
|
39
37
|
return window.xtermAddons.WebLinksAddon
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
export async function loadCanvasAddon () {
|
|
43
41
|
if (window.xtermAddons.CanvasAddon) return window.xtermAddons.CanvasAddon
|
|
44
|
-
const mod = await
|
|
42
|
+
const mod = await import('@xterm/addon-canvas')
|
|
45
43
|
window.xtermAddons.CanvasAddon = mod.CanvasAddon
|
|
46
44
|
return window.xtermAddons.CanvasAddon
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
export async function loadWebglAddon () {
|
|
50
48
|
if (window.xtermAddons.WebglAddon) return window.xtermAddons.WebglAddon
|
|
51
|
-
const mod = await
|
|
49
|
+
const mod = await import('@xterm/addon-webgl')
|
|
52
50
|
window.xtermAddons.WebglAddon = mod.WebglAddon
|
|
53
51
|
return window.xtermAddons.WebglAddon
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
export async function loadSearchAddon () {
|
|
57
55
|
if (window.xtermAddons.SearchAddon) return window.xtermAddons.SearchAddon
|
|
58
|
-
const mod = await
|
|
56
|
+
const mod = await import('@xterm/addon-search')
|
|
59
57
|
window.xtermAddons.SearchAddon = mod.SearchAddon
|
|
60
58
|
return window.xtermAddons.SearchAddon
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
export async function loadLigaturesAddon () {
|
|
64
62
|
if (window.xtermAddons.LigaturesAddon) return window.xtermAddons.LigaturesAddon
|
|
65
|
-
const mod = await
|
|
63
|
+
const mod = await import('@xterm/addon-ligatures')
|
|
66
64
|
window.xtermAddons.LigaturesAddon = mod.LigaturesAddon
|
|
67
65
|
return window.xtermAddons.LigaturesAddon
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
export async function loadUnicode11Addon () {
|
|
71
69
|
if (window.xtermAddons.Unicode11Addon) return window.xtermAddons.Unicode11Addon
|
|
72
|
-
const mod = await
|
|
70
|
+
const mod = await import('@xterm/addon-unicode11')
|
|
73
71
|
window.xtermAddons.Unicode11Addon = mod.Unicode11Addon
|
|
74
72
|
return window.xtermAddons.Unicode11Addon
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
export async function loadImageAddon () {
|
|
78
76
|
if (window.xtermAddons.ImageAddon) return window.xtermAddons.ImageAddon
|
|
79
|
-
const mod = await
|
|
77
|
+
const mod = await import('@xterm/addon-image')
|
|
80
78
|
window.xtermAddons.ImageAddon = mod.ImageAddon
|
|
81
79
|
return window.xtermAddons.ImageAddon
|
|
82
80
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { lazy, Suspense } from 'react'
|
|
2
|
-
import importRetry from '../../common/import-retry'
|
|
3
2
|
|
|
4
|
-
const TerminalInfo = lazy(() =>
|
|
3
|
+
const TerminalInfo = lazy(() => import('./terminal-info'))
|
|
5
4
|
|
|
6
5
|
export default function TerminalInfoEntry (props) {
|
|
7
6
|
return (
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { lazy, Suspense } from 'react'
|
|
2
|
-
import importRetry from '../../common/import-retry'
|
|
3
2
|
|
|
4
|
-
const TextEditor = lazy(() =>
|
|
3
|
+
const TextEditor = lazy(() => import('./text-editor'))
|
|
5
4
|
|
|
6
5
|
export default function TextEditorEntry (props) {
|
|
7
6
|
return (
|
|
@@ -18,14 +18,13 @@ import Modal from '../common/modal'
|
|
|
18
18
|
import { copy } from '../../common/clipboard'
|
|
19
19
|
import VncForm from './vnc-form'
|
|
20
20
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
21
|
-
import importRetry from '../../common/import-retry'
|
|
22
21
|
import './vnc.styl'
|
|
23
22
|
|
|
24
23
|
// noVNC module imports — loaded dynamically
|
|
25
24
|
async function loadVncModule () {
|
|
26
25
|
if (window.novnc) return
|
|
27
26
|
console.debug('[VNC-CLIENT] Loading noVNC module...')
|
|
28
|
-
const mod = await
|
|
27
|
+
const mod = await import('@novnc/novnc/core/rfb')
|
|
29
28
|
window.novnc = {
|
|
30
29
|
RFB: mod.default
|
|
31
30
|
}
|
package/client/css/basic.styl
CHANGED
package/client/store/sidebar.js
CHANGED
package/client/store/sync.js
CHANGED
|
@@ -234,7 +234,7 @@ export default (Store) => {
|
|
|
234
234
|
const status = statusContent ? parseJsonSafe(statusContent) : undefined
|
|
235
235
|
store.syncServerStatus[type] = status
|
|
236
236
|
}
|
|
237
|
-
return
|
|
237
|
+
return gist
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
const gist = await fetchData(
|
|
@@ -245,6 +245,103 @@ export default (Store) => {
|
|
|
245
245
|
store.getSyncProxy(type)
|
|
246
246
|
)
|
|
247
247
|
updateSyncServerStatusFromGist(store, gist, type)
|
|
248
|
+
return gist
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
Store.prototype.previewServerDataWithCompare = async function (type) {
|
|
252
|
+
const { store } = window
|
|
253
|
+
const token = store.getSyncToken(type)
|
|
254
|
+
const gistId = store.getSyncGistId(type)
|
|
255
|
+
const pass = store.getSyncPassword(type)
|
|
256
|
+
const { names } = store.getDataSyncNames()
|
|
257
|
+
|
|
258
|
+
// Get server data
|
|
259
|
+
let serverGist
|
|
260
|
+
if (type === syncTypes.webdav) {
|
|
261
|
+
serverGist = await fetchData(
|
|
262
|
+
type,
|
|
263
|
+
'download',
|
|
264
|
+
[],
|
|
265
|
+
token,
|
|
266
|
+
store.getSyncProxy(type)
|
|
267
|
+
)
|
|
268
|
+
} else {
|
|
269
|
+
serverGist = await fetchData(
|
|
270
|
+
type,
|
|
271
|
+
'getOne',
|
|
272
|
+
[gistId],
|
|
273
|
+
token,
|
|
274
|
+
store.getSyncProxy(type)
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Update status
|
|
279
|
+
if (type === syncTypes.webdav) {
|
|
280
|
+
if (serverGist && serverGist.files) {
|
|
281
|
+
const statusContent = get(serverGist, 'files["electerm-status.json"].content')
|
|
282
|
+
const status = statusContent ? parseJsonSafe(statusContent) : undefined
|
|
283
|
+
store.syncServerStatus[type] = status
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
updateSyncServerStatusFromGist(store, serverGist, type)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Compare data
|
|
290
|
+
const comparison = []
|
|
291
|
+
const localData = {}
|
|
292
|
+
const serverData = {}
|
|
293
|
+
|
|
294
|
+
for (const n of names) {
|
|
295
|
+
// Get local data
|
|
296
|
+
const localItems = store.getItems(n)
|
|
297
|
+
localData[n] = localItems
|
|
298
|
+
|
|
299
|
+
// Get server data
|
|
300
|
+
let serverStr
|
|
301
|
+
if (type === syncTypes.webdav) {
|
|
302
|
+
serverStr = get(serverGist, `files["${n}.json"].content`)
|
|
303
|
+
} else {
|
|
304
|
+
serverStr = get(serverGist, `files["${n}.json"].content`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let serverItems = []
|
|
308
|
+
if (serverStr) {
|
|
309
|
+
try {
|
|
310
|
+
if (!isJSON(serverStr)) {
|
|
311
|
+
serverStr = await window.pre.runGlobalAsync('decryptAsync', serverStr, pass)
|
|
312
|
+
}
|
|
313
|
+
serverItems = JSON.parse(serverStr)
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error(`Failed to parse server data for ${n}:`, e)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
serverData[n] = serverItems
|
|
319
|
+
|
|
320
|
+
// Find unique items
|
|
321
|
+
const localIds = new Set(localItems.map(item => item.id))
|
|
322
|
+
const serverIds = new Set(serverItems.map(item => item.id))
|
|
323
|
+
|
|
324
|
+
const onlyLocal = localItems.filter(item => !serverIds.has(item.id))
|
|
325
|
+
const onlyServer = serverItems.filter(item => !localIds.has(item.id))
|
|
326
|
+
const common = localItems.filter(item => serverIds.has(item.id))
|
|
327
|
+
|
|
328
|
+
comparison.push({
|
|
329
|
+
name: n,
|
|
330
|
+
localCount: localItems.length,
|
|
331
|
+
serverCount: serverItems.length,
|
|
332
|
+
onlyLocal: onlyLocal.length,
|
|
333
|
+
onlyServer: onlyServer.length,
|
|
334
|
+
common: common.length,
|
|
335
|
+
localItems: onlyLocal,
|
|
336
|
+
serverItems: onlyServer
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
localData,
|
|
342
|
+
serverData,
|
|
343
|
+
comparison
|
|
344
|
+
}
|
|
248
345
|
}
|
|
249
346
|
|
|
250
347
|
Store.prototype.uploadSettingAction = async function (type) {
|
package/package.json
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Retry wrapper for dynamic import() calls
|
|
3
|
-
* Handles transient "Failed to fetch" errors that can occur
|
|
4
|
-
* when the app starts and chunks are fetched before the
|
|
5
|
-
* network/server is fully ready
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const MAX_RETRIES = 3
|
|
9
|
-
const RETRY_DELAY = 500
|
|
10
|
-
|
|
11
|
-
function sleep (ms) {
|
|
12
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default function importRetry (factory, retries = MAX_RETRIES) {
|
|
16
|
-
return factory().catch(async (err) => {
|
|
17
|
-
if (retries <= 0) {
|
|
18
|
-
throw err
|
|
19
|
-
}
|
|
20
|
-
await sleep(RETRY_DELAY)
|
|
21
|
-
return importRetry(factory, retries - 1)
|
|
22
|
-
})
|
|
23
|
-
}
|