@electerm/electerm-react 2.17.8 → 3.0.6
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/cache.js +4 -2
- package/client/common/db.js +3 -5
- package/client/common/default-setting.js +0 -1
- package/client/common/pass-enc.js +0 -21
- package/client/common/safe-local-storage.js +106 -1
- package/client/components/ai/ai-chat-history-item.jsx +132 -8
- package/client/components/ai/ai-chat.jsx +10 -159
- package/client/components/ai/ai.styl +6 -1
- package/client/components/bookmark-form/common/render-auth-ssh.jsx +1 -117
- package/client/components/bookmark-form/config/common-fields.js +8 -0
- package/client/components/bookmark-form/config/local.js +10 -51
- package/client/components/setting-panel/setting-terminal.jsx +0 -1
- package/client/components/sftp/sftp-entry.jsx +35 -12
- package/client/components/sidebar/history.jsx +1 -1
- package/client/components/tabs/app-drag.jsx +13 -12
- package/client/components/terminal/highlight-addon.js +1 -1
- package/client/components/terminal/terminal.jsx +9 -14
- package/client/components/terminal/transfer-client-base.js +3 -3
- package/client/components/text-editor/edit-with-custom-editor.jsx +3 -2
- package/client/components/text-editor/simple-editor.jsx +1 -3
- package/client/components/widgets/widget-form.jsx +4 -3
- package/client/store/init-state.js +3 -3
- package/client/store/sync.js +0 -1
- package/client/store/watch.js +3 -3
- package/package.json +1 -1
package/client/common/cache.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// keep limit items in cache
|
|
3
3
|
// we persist cache to local storage, so we can keep cache after restart
|
|
4
4
|
|
|
5
|
+
import { safeGetItem, safeSetItem } from './safe-local-storage.js'
|
|
6
|
+
|
|
5
7
|
class MapCache {
|
|
6
8
|
constructor (limit, key) {
|
|
7
9
|
this.limit = limit
|
|
@@ -11,7 +13,7 @@ class MapCache {
|
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
load () {
|
|
14
|
-
const data =
|
|
16
|
+
const data = safeGetItem(this.key)
|
|
15
17
|
if (data) {
|
|
16
18
|
const arr = JSON.parse(data)
|
|
17
19
|
for (const item of arr) {
|
|
@@ -28,7 +30,7 @@ class MapCache {
|
|
|
28
30
|
value
|
|
29
31
|
})
|
|
30
32
|
}
|
|
31
|
-
|
|
33
|
+
safeSetItem(this.key, JSON.stringify(arr))
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
set (key, value) {
|
package/client/common/db.js
CHANGED
|
@@ -9,7 +9,7 @@ import { without, isArray } from 'lodash-es'
|
|
|
9
9
|
import handleError from './error-handler'
|
|
10
10
|
import generate from './uid'
|
|
11
11
|
import safeParse from './to-simple-obj'
|
|
12
|
-
import {
|
|
12
|
+
import { decObj } from './pass-enc'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* db action, never direct use it
|
|
@@ -46,7 +46,7 @@ export function insert (dbName, inst) {
|
|
|
46
46
|
const { id, _id, ...rest } = obj
|
|
47
47
|
return {
|
|
48
48
|
_id: _id || id || generate(),
|
|
49
|
-
...
|
|
49
|
+
...rest
|
|
50
50
|
}
|
|
51
51
|
})
|
|
52
52
|
return dbAction(dbName, 'insert', safeParse(arr))
|
|
@@ -76,9 +76,7 @@ export async function remove (dbName, id) {
|
|
|
76
76
|
export function update (_id, value, db = 'data', upsert = true) {
|
|
77
77
|
const updates = dbNames.includes(db)
|
|
78
78
|
? {
|
|
79
|
-
$set:
|
|
80
|
-
...encObj(value)
|
|
81
|
-
}
|
|
79
|
+
$set: value
|
|
82
80
|
}
|
|
83
81
|
: {
|
|
84
82
|
$set: {
|
|
@@ -29,7 +29,6 @@ export default {
|
|
|
29
29
|
terminalBackgroundTextColor: '#ffffff',
|
|
30
30
|
terminalBackgroundTextFontFamily: 'Maple Mono',
|
|
31
31
|
rendererType: 'canvas',
|
|
32
|
-
enableSixel: true,
|
|
33
32
|
terminalType: 'xterm-256color',
|
|
34
33
|
keepaliveCountMax: 10,
|
|
35
34
|
keyword2FA: 'verification code,otp,one-time,two-factor,2fa,totp,authenticator,duo,yubikey,security code,mfa,passcode',
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
const enc = (str) => {
|
|
2
|
-
if (typeof str !== 'string') {
|
|
3
|
-
return str
|
|
4
|
-
}
|
|
5
|
-
return str.split('').map((s, i) => {
|
|
6
|
-
return String.fromCharCode((s.charCodeAt(0) + i + 1) % 65536)
|
|
7
|
-
}).join('')
|
|
8
|
-
}
|
|
9
|
-
|
|
10
1
|
const dec = (str) => {
|
|
11
2
|
if (typeof str !== 'string') {
|
|
12
3
|
return str
|
|
@@ -16,18 +7,6 @@ const dec = (str) => {
|
|
|
16
7
|
}).join('')
|
|
17
8
|
}
|
|
18
9
|
|
|
19
|
-
/**
|
|
20
|
-
* enc password
|
|
21
|
-
* @param {object} obj
|
|
22
|
-
*/
|
|
23
|
-
export function encObj (obj) {
|
|
24
|
-
if (!obj.passwordEncrypted && obj.password) {
|
|
25
|
-
obj.password = enc(obj.password)
|
|
26
|
-
obj.passwordEncrypted = true
|
|
27
|
-
}
|
|
28
|
-
return obj
|
|
29
|
-
}
|
|
30
|
-
|
|
31
10
|
/**
|
|
32
11
|
* dec password
|
|
33
12
|
* @param {object} obj
|
|
@@ -1,4 +1,60 @@
|
|
|
1
1
|
import { termLSPrefix } from './constants'
|
|
2
|
+
import parseJsonSafe from './parse-json-safe'
|
|
3
|
+
|
|
4
|
+
// ─── Encryption helpers ───────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
// Prefix that marks a value encrypted by this module.
|
|
7
|
+
// Values without this prefix are treated as legacy plaintext.
|
|
8
|
+
const ENC_PREFIX = 'enc1:'
|
|
9
|
+
|
|
10
|
+
let _encKey = null
|
|
11
|
+
|
|
12
|
+
function getKey () {
|
|
13
|
+
if (_encKey !== null) return _encKey
|
|
14
|
+
try {
|
|
15
|
+
// window.pre is set by pre.js before the store is used; runSync is synchronous IPC
|
|
16
|
+
_encKey = (window.pre && window.pre.runSync && window.pre.runSync('getStorageKey')) || ''
|
|
17
|
+
} catch (e) {
|
|
18
|
+
_encKey = ''
|
|
19
|
+
}
|
|
20
|
+
return _encKey
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function encrypt (str) {
|
|
24
|
+
if (!str) return str
|
|
25
|
+
const key = getKey()
|
|
26
|
+
if (!key) return str
|
|
27
|
+
const strBytes = new TextEncoder().encode(str)
|
|
28
|
+
const keyBytes = new TextEncoder().encode(key)
|
|
29
|
+
const out = new Uint8Array(strBytes.length)
|
|
30
|
+
for (let i = 0; i < strBytes.length; i++) {
|
|
31
|
+
out[i] = strBytes[i] ^ keyBytes[i % keyBytes.length]
|
|
32
|
+
}
|
|
33
|
+
let binary = ''
|
|
34
|
+
for (let i = 0; i < out.length; i++) {
|
|
35
|
+
binary += String.fromCharCode(out[i])
|
|
36
|
+
}
|
|
37
|
+
return ENC_PREFIX + btoa(binary)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function decrypt (str) {
|
|
41
|
+
if (!str || !str.startsWith(ENC_PREFIX)) return str
|
|
42
|
+
const key = getKey()
|
|
43
|
+
if (!key) return str
|
|
44
|
+
try {
|
|
45
|
+
const binary = atob(str.slice(ENC_PREFIX.length))
|
|
46
|
+
const keyBytes = new TextEncoder().encode(key)
|
|
47
|
+
const out = new Uint8Array(binary.length)
|
|
48
|
+
for (let i = 0; i < binary.length; i++) {
|
|
49
|
+
out[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length]
|
|
50
|
+
}
|
|
51
|
+
return new TextDecoder().decode(out)
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return str
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Internal helper ─────────────────────────────────────────────────────────
|
|
2
58
|
|
|
3
59
|
function clear () {
|
|
4
60
|
const keys = Object.keys(window.localStorage)
|
|
@@ -9,6 +65,8 @@ function clear () {
|
|
|
9
65
|
}
|
|
10
66
|
}
|
|
11
67
|
|
|
68
|
+
// ─── Original (plain) functions ──────────────────────────────────────────────
|
|
69
|
+
|
|
12
70
|
export function setItem (id, str) {
|
|
13
71
|
try {
|
|
14
72
|
window.localStorage.setItem(id, str)
|
|
@@ -26,10 +84,57 @@ export function getItem (id) {
|
|
|
26
84
|
|
|
27
85
|
export function getItemJSON (id, defaultValue) {
|
|
28
86
|
const str = window.localStorage.getItem(id) || ''
|
|
29
|
-
|
|
87
|
+
const r = parseJsonSafe(str)
|
|
88
|
+
if (typeof r === 'string') {
|
|
89
|
+
return defaultValue
|
|
90
|
+
}
|
|
91
|
+
return r || defaultValue
|
|
30
92
|
}
|
|
31
93
|
|
|
32
94
|
export function setItemJSON (id, obj) {
|
|
33
95
|
const str = JSON.stringify(obj)
|
|
34
96
|
return setItem(id, str)
|
|
35
97
|
}
|
|
98
|
+
|
|
99
|
+
// ─── Safe (encrypted) functions ──────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export function safeSetItem (id, str) {
|
|
102
|
+
if (window.et.isWebApp) {
|
|
103
|
+
return setItem(id, str)
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
window.localStorage.setItem(id, encrypt(str))
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.log(e)
|
|
109
|
+
console.log('maybe local storage full, lets reset')
|
|
110
|
+
clear()
|
|
111
|
+
window.localStorage.setItem(id, encrypt(str))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function safeGetItem (id) {
|
|
116
|
+
if (window.et.isWebApp) {
|
|
117
|
+
return getItem(id)
|
|
118
|
+
}
|
|
119
|
+
return decrypt(window.localStorage.getItem(id) || '')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function safeGetItemJSON (id, defaultValue) {
|
|
123
|
+
if (window.et.isWebApp) {
|
|
124
|
+
return getItemJSON(id, defaultValue)
|
|
125
|
+
}
|
|
126
|
+
const str = decrypt(window.localStorage.getItem(id) || '')
|
|
127
|
+
const r = parseJsonSafe(str)
|
|
128
|
+
if (typeof r === 'string') {
|
|
129
|
+
return defaultValue
|
|
130
|
+
}
|
|
131
|
+
return r || defaultValue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function safeSetItemJSON (id, obj) {
|
|
135
|
+
if (window.et.isWebApp) {
|
|
136
|
+
return setItemJSON(id, obj)
|
|
137
|
+
}
|
|
138
|
+
const str = JSON.stringify(obj)
|
|
139
|
+
return safeSetItem(id, str)
|
|
140
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
2
|
import AIOutput from './ai-output'
|
|
3
|
+
import AIStopIcon from './ai-stop-icon'
|
|
3
4
|
import {
|
|
4
5
|
Alert,
|
|
5
6
|
Tooltip
|
|
@@ -12,47 +13,169 @@ import {
|
|
|
12
13
|
CaretRightOutlined
|
|
13
14
|
} from '@ant-design/icons'
|
|
14
15
|
import { copy } from '../../common/clipboard'
|
|
15
|
-
import { useState } from 'react'
|
|
16
16
|
|
|
17
17
|
export default function AIChatHistoryItem ({ item }) {
|
|
18
18
|
const [showOutput, setShowOutput] = useState(true)
|
|
19
|
+
const startedRef = useRef(false)
|
|
19
20
|
const {
|
|
20
|
-
prompt
|
|
21
|
+
prompt,
|
|
22
|
+
isStreaming,
|
|
23
|
+
sessionId,
|
|
24
|
+
response,
|
|
25
|
+
modelAI,
|
|
26
|
+
roleAI,
|
|
27
|
+
baseURLAI,
|
|
28
|
+
apiPathAI,
|
|
29
|
+
apiKeyAI,
|
|
30
|
+
proxyAI,
|
|
31
|
+
languageAI
|
|
21
32
|
} = item
|
|
22
33
|
|
|
23
34
|
function toggleOutput () {
|
|
24
35
|
setShowOutput(!showOutput)
|
|
25
36
|
}
|
|
26
37
|
|
|
38
|
+
function buildRole () {
|
|
39
|
+
const lang = languageAI || window.store.getLangName()
|
|
40
|
+
return roleAI + `;用[${lang}]回复`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pollStreamContent = useCallback(async (sid) => {
|
|
44
|
+
try {
|
|
45
|
+
const streamResponse = await window.pre.runGlobalAsync('getStreamContent', sid)
|
|
46
|
+
|
|
47
|
+
if (streamResponse && streamResponse.error) {
|
|
48
|
+
if (streamResponse.error === 'Session not found') {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
window.store.removeAiHistory(item.id)
|
|
52
|
+
return window.store.onError(new Error(streamResponse.error))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
56
|
+
if (index !== -1) {
|
|
57
|
+
window.store.aiChatHistory[index].response = streamResponse.content || ''
|
|
58
|
+
window.store.aiChatHistory[index].isStreaming = streamResponse.hasMore
|
|
59
|
+
window.store.aiChatHistory = [...window.store.aiChatHistory]
|
|
60
|
+
|
|
61
|
+
if (streamResponse.hasMore) {
|
|
62
|
+
setTimeout(() => pollStreamContent(sid), 200)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
window.store.removeAiHistory(item.id)
|
|
67
|
+
window.store.onError(error)
|
|
68
|
+
}
|
|
69
|
+
}, [item.id])
|
|
70
|
+
|
|
71
|
+
const startRequest = useCallback(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const aiResponse = await window.pre.runGlobalAsync(
|
|
74
|
+
'AIchat',
|
|
75
|
+
prompt,
|
|
76
|
+
modelAI,
|
|
77
|
+
buildRole(),
|
|
78
|
+
baseURLAI,
|
|
79
|
+
apiPathAI,
|
|
80
|
+
apiKeyAI,
|
|
81
|
+
proxyAI,
|
|
82
|
+
true
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (aiResponse && aiResponse.error) {
|
|
86
|
+
window.store.removeAiHistory(item.id)
|
|
87
|
+
return window.store.onError(new Error(aiResponse.error))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (aiResponse && aiResponse.isStream && aiResponse.sessionId) {
|
|
91
|
+
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
92
|
+
if (index !== -1) {
|
|
93
|
+
window.store.aiChatHistory[index].isStreaming = true
|
|
94
|
+
window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
|
|
95
|
+
window.store.aiChatHistory[index].response = aiResponse.content || ''
|
|
96
|
+
}
|
|
97
|
+
pollStreamContent(aiResponse.sessionId)
|
|
98
|
+
} else if (aiResponse && aiResponse.response) {
|
|
99
|
+
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
100
|
+
if (index !== -1) {
|
|
101
|
+
window.store.aiChatHistory[index].response = aiResponse.response
|
|
102
|
+
window.store.aiChatHistory[index].isStreaming = false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
window.store.removeAiHistory(item.id)
|
|
107
|
+
window.store.onError(error)
|
|
108
|
+
}
|
|
109
|
+
}, [prompt, modelAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, item.id, pollStreamContent])
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!response && !startedRef.current) {
|
|
113
|
+
startedRef.current = true
|
|
114
|
+
startRequest()
|
|
115
|
+
}
|
|
116
|
+
}, [])
|
|
117
|
+
|
|
118
|
+
async function handleStop (e) {
|
|
119
|
+
e.stopPropagation()
|
|
120
|
+
if (!sessionId) return
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await window.pre.runGlobalAsync('stopStream', sessionId)
|
|
124
|
+
const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
|
|
125
|
+
if (index !== -1) {
|
|
126
|
+
window.store.aiChatHistory[index].isStreaming = false
|
|
127
|
+
window.store.aiChatHistory = [...window.store.aiChatHistory]
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Error stopping stream:', error)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderStopButton () {
|
|
135
|
+
if (!isStreaming) {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
return (
|
|
139
|
+
<AIStopIcon
|
|
140
|
+
onClick={handleStop}
|
|
141
|
+
title='Stop this AI request'
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
27
146
|
const alertProps = {
|
|
28
147
|
title: (
|
|
29
|
-
|
|
148
|
+
<div className='ai-history-item-title'>
|
|
30
149
|
<span className='pointer mg1r' onClick={toggleOutput}>
|
|
31
150
|
{showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
|
32
151
|
</span>
|
|
33
152
|
<UserOutlined />: {prompt}
|
|
34
|
-
|
|
153
|
+
{renderStopButton()}
|
|
154
|
+
</div>
|
|
35
155
|
),
|
|
36
156
|
type: 'info'
|
|
37
157
|
}
|
|
158
|
+
|
|
38
159
|
function handleDel (e) {
|
|
39
160
|
e.stopPropagation()
|
|
40
161
|
window.store.removeAiHistory(item.id)
|
|
41
162
|
}
|
|
163
|
+
|
|
42
164
|
function handleCopy () {
|
|
43
165
|
copy(prompt)
|
|
44
166
|
}
|
|
167
|
+
|
|
45
168
|
function renderTitle () {
|
|
46
169
|
return (
|
|
47
170
|
<div>
|
|
48
171
|
<p>
|
|
49
|
-
<b>Model:</b> {
|
|
172
|
+
<b>Model:</b> {modelAI}
|
|
50
173
|
</p>
|
|
51
174
|
<p>
|
|
52
|
-
<b>Role:</b> {
|
|
175
|
+
<b>Role:</b> {roleAI}
|
|
53
176
|
</p>
|
|
54
177
|
<p>
|
|
55
|
-
<b>Base URL:</b> {
|
|
178
|
+
<b>Base URL:</b> {baseURLAI}
|
|
56
179
|
</p>
|
|
57
180
|
<p>
|
|
58
181
|
<b>Time:</b> {new Date(item.timestamp).toLocaleString()}
|
|
@@ -70,6 +193,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
70
193
|
</div>
|
|
71
194
|
)
|
|
72
195
|
}
|
|
196
|
+
|
|
73
197
|
return (
|
|
74
198
|
<div className='chat-history-item'>
|
|
75
199
|
<div className='mg1y'>
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from '../../common/constants'
|
|
15
15
|
import HelpIcon from '../common/help-icon'
|
|
16
16
|
import { refsStatic } from '../common/ref'
|
|
17
|
-
import AIStopIcon from './ai-stop-icon'
|
|
18
17
|
import './ai.styl'
|
|
19
18
|
|
|
20
19
|
const { TextArea } = Input
|
|
@@ -22,155 +21,43 @@ const MAX_HISTORY = 100
|
|
|
22
21
|
|
|
23
22
|
export default function AIChat (props) {
|
|
24
23
|
const [prompt, setPrompt] = useState('')
|
|
25
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
26
|
-
const [currentSessionId, setCurrentSessionId] = useState(null)
|
|
27
24
|
|
|
28
25
|
function handlePromptChange (e) {
|
|
29
26
|
setPrompt(e.target.value)
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
const lang = props.config.languageAI || window.store.getLangName()
|
|
34
|
-
return props.config.roleAI + `;用[${lang}]回复`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const handleSubmit = useCallback(async function () {
|
|
29
|
+
const handleSubmit = useCallback(function () {
|
|
38
30
|
if (window.store.aiConfigMissing()) {
|
|
39
31
|
window.store.toggleAIConfig()
|
|
40
32
|
}
|
|
41
|
-
if (!prompt.trim()
|
|
42
|
-
setIsLoading(true)
|
|
33
|
+
if (!prompt.trim()) return
|
|
43
34
|
|
|
44
|
-
// Create a placeholder entry for the streaming response
|
|
45
35
|
const chatId = uid()
|
|
46
36
|
const chatEntry = {
|
|
47
37
|
prompt,
|
|
48
|
-
response: '',
|
|
38
|
+
response: '',
|
|
49
39
|
isStreaming: false,
|
|
50
40
|
sessionId: null,
|
|
51
41
|
...pick(props.config, [
|
|
52
42
|
'modelAI',
|
|
53
43
|
'roleAI',
|
|
54
|
-
'baseURLAI'
|
|
44
|
+
'baseURLAI',
|
|
45
|
+
'apiPathAI',
|
|
46
|
+
'apiKeyAI',
|
|
47
|
+
'proxyAI',
|
|
48
|
+
'languageAI'
|
|
55
49
|
]),
|
|
56
50
|
timestamp: Date.now(),
|
|
57
51
|
id: chatId
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
window.store.aiChatHistory.push(chatEntry)
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const aiResponse = await window.pre.runGlobalAsync(
|
|
64
|
-
'AIchat',
|
|
65
|
-
prompt,
|
|
66
|
-
props.config.modelAI,
|
|
67
|
-
buildRole(),
|
|
68
|
-
props.config.baseURLAI,
|
|
69
|
-
props.config.apiPathAI,
|
|
70
|
-
props.config.apiKeyAI,
|
|
71
|
-
props.config.proxyAI,
|
|
72
|
-
true // Enable streaming for chat
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
if (aiResponse && aiResponse.error) {
|
|
76
|
-
// Remove the placeholder entry and show error
|
|
77
|
-
const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
|
|
78
|
-
if (index !== -1) {
|
|
79
|
-
window.store.aiChatHistory.splice(index, 1)
|
|
80
|
-
}
|
|
81
|
-
setIsLoading(false)
|
|
82
|
-
return window.store.onError(
|
|
83
|
-
new Error(aiResponse.error)
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (aiResponse && aiResponse.isStream && aiResponse.sessionId) {
|
|
88
|
-
// Handle streaming response with polling
|
|
89
|
-
const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
|
|
90
|
-
if (index !== -1) {
|
|
91
|
-
window.store.aiChatHistory[index].isStreaming = true
|
|
92
|
-
window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
|
|
93
|
-
window.store.aiChatHistory[index].response = aiResponse.content || ''
|
|
94
|
-
}
|
|
95
|
-
// Store current session ID for stop functionality
|
|
96
|
-
setCurrentSessionId(aiResponse.sessionId)
|
|
97
|
-
|
|
98
|
-
// Start polling for updates
|
|
99
|
-
pollStreamContent(aiResponse.sessionId, chatId)
|
|
100
|
-
} else if (aiResponse && aiResponse.response) {
|
|
101
|
-
// Handle non-streaming response (fallback)
|
|
102
|
-
const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
|
|
103
|
-
if (index !== -1) {
|
|
104
|
-
window.store.aiChatHistory[index].response = aiResponse.response
|
|
105
|
-
window.store.aiChatHistory[index].isStreaming = false
|
|
106
|
-
}
|
|
107
|
-
setIsLoading(false)
|
|
108
|
-
}
|
|
109
|
-
} catch (error) {
|
|
110
|
-
// Remove the placeholder entry and show error
|
|
111
|
-
const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
|
|
112
|
-
if (index !== -1) {
|
|
113
|
-
window.store.aiChatHistory.splice(index, 1)
|
|
114
|
-
}
|
|
115
|
-
setIsLoading(false)
|
|
116
|
-
window.store.onError(error)
|
|
117
|
-
}
|
|
55
|
+
setPrompt('')
|
|
118
56
|
|
|
119
57
|
if (window.store.aiChatHistory.length > MAX_HISTORY) {
|
|
120
58
|
window.store.aiChatHistory.splice(MAX_HISTORY)
|
|
121
59
|
}
|
|
122
|
-
|
|
123
|
-
}, [prompt, isLoading])
|
|
124
|
-
|
|
125
|
-
// Function to poll for streaming content updates
|
|
126
|
-
const pollStreamContent = async (sessionId, chatId) => {
|
|
127
|
-
try {
|
|
128
|
-
const streamResponse = await window.pre.runGlobalAsync('getStreamContent', sessionId)
|
|
129
|
-
|
|
130
|
-
if (streamResponse && streamResponse.error) {
|
|
131
|
-
// Session not found or error - stop polling
|
|
132
|
-
if (streamResponse.error === 'Session not found') {
|
|
133
|
-
setCurrentSessionId(null)
|
|
134
|
-
setIsLoading(false)
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
// Remove the entry and show error
|
|
138
|
-
const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
|
|
139
|
-
if (index !== -1) {
|
|
140
|
-
window.store.aiChatHistory.splice(index, 1)
|
|
141
|
-
}
|
|
142
|
-
setIsLoading(false)
|
|
143
|
-
return window.store.onError(new Error(streamResponse.error))
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Update the chat entry with new content
|
|
147
|
-
const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
|
|
148
|
-
if (index !== -1) {
|
|
149
|
-
window.store.aiChatHistory[index].response = streamResponse.content || ''
|
|
150
|
-
window.store.aiChatHistory[index].isStreaming = streamResponse.hasMore
|
|
151
|
-
|
|
152
|
-
// Force re-render by updating the array reference
|
|
153
|
-
window.store.aiChatHistory = [...window.store.aiChatHistory]
|
|
154
|
-
|
|
155
|
-
// Continue polling if there's more content
|
|
156
|
-
if (streamResponse.hasMore) {
|
|
157
|
-
setTimeout(() => pollStreamContent(sessionId, chatId), 200) // Poll every 200ms
|
|
158
|
-
} else {
|
|
159
|
-
setCurrentSessionId(null)
|
|
160
|
-
setIsLoading(false)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
} catch (error) {
|
|
164
|
-
// Remove the entry and show error
|
|
165
|
-
const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
|
|
166
|
-
if (index !== -1) {
|
|
167
|
-
window.store.aiChatHistory.splice(index, 1)
|
|
168
|
-
}
|
|
169
|
-
setCurrentSessionId(null)
|
|
170
|
-
setIsLoading(false)
|
|
171
|
-
window.store.onError(error)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
60
|
+
}, [prompt])
|
|
174
61
|
|
|
175
62
|
function renderHistory () {
|
|
176
63
|
return (
|
|
@@ -188,41 +75,7 @@ export default function AIChat (props) {
|
|
|
188
75
|
window.store.aiChatHistory = []
|
|
189
76
|
}
|
|
190
77
|
|
|
191
|
-
const handleStop = useCallback(async function () {
|
|
192
|
-
if (!currentSessionId || !isLoading) return
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
// Call server to stop the stream
|
|
196
|
-
await window.pre.runGlobalAsync('stopStream', currentSessionId)
|
|
197
|
-
|
|
198
|
-
// Reset state
|
|
199
|
-
setCurrentSessionId(null)
|
|
200
|
-
setIsLoading(false)
|
|
201
|
-
|
|
202
|
-
// Update the chat entry to mark as stopped
|
|
203
|
-
const chatEntries = window.store.aiChatHistory
|
|
204
|
-
for (let i = chatEntries.length - 1; i >= 0; i--) {
|
|
205
|
-
if (chatEntries[i].isStreaming) {
|
|
206
|
-
chatEntries[i].isStreaming = false
|
|
207
|
-
break
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
window.store.aiChatHistory = [...chatEntries]
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error('Error stopping stream:', error)
|
|
213
|
-
setCurrentSessionId(null)
|
|
214
|
-
setIsLoading(false)
|
|
215
|
-
}
|
|
216
|
-
}, [currentSessionId, isLoading])
|
|
217
|
-
|
|
218
78
|
function renderSendIcon () {
|
|
219
|
-
if (isLoading) {
|
|
220
|
-
return (
|
|
221
|
-
<AIStopIcon
|
|
222
|
-
onClick={handleStop}
|
|
223
|
-
/>
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
79
|
return (
|
|
227
80
|
<SendOutlined
|
|
228
81
|
onClick={handleSubmit}
|
|
@@ -254,7 +107,6 @@ export default function AIChat (props) {
|
|
|
254
107
|
e.preventDefault()
|
|
255
108
|
handleSubmit()
|
|
256
109
|
}
|
|
257
|
-
// If Shift+Enter, allow default behavior (new line)
|
|
258
110
|
}
|
|
259
111
|
|
|
260
112
|
return (
|
|
@@ -270,7 +122,6 @@ export default function AIChat (props) {
|
|
|
270
122
|
onPressEnter={handleKeyPress}
|
|
271
123
|
placeholder='Enter your prompt here'
|
|
272
124
|
autoSize={{ minRows: 3, maxRows: 10 }}
|
|
273
|
-
disabled={isLoading}
|
|
274
125
|
className='ai-chat-textarea'
|
|
275
126
|
/>
|
|
276
127
|
<Flex className='ai-chat-terminals' justify='space-between' align='center'>
|
|
@@ -66,9 +66,14 @@
|
|
|
66
66
|
.ai-stop-icon-square
|
|
67
67
|
width 20px
|
|
68
68
|
height 20px
|
|
69
|
-
background var(--error)
|
|
70
69
|
border-radius 2px
|
|
71
70
|
display flex
|
|
72
71
|
align-items center
|
|
73
72
|
justify-content center
|
|
74
73
|
font-size 10px
|
|
74
|
+
|
|
75
|
+
.ai-history-item-title
|
|
76
|
+
display flex
|
|
77
|
+
align-items center
|
|
78
|
+
.ai-stop-icon-square
|
|
79
|
+
margin-left auto
|
|
@@ -1,117 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* bookmark form auth renderer (copied from legacy)
|
|
3
|
-
*/
|
|
4
|
-
import {
|
|
5
|
-
Button,
|
|
6
|
-
Input,
|
|
7
|
-
AutoComplete,
|
|
8
|
-
Form,
|
|
9
|
-
Select
|
|
10
|
-
} from 'antd'
|
|
11
|
-
import { formItemLayout } from '../../../common/form-layout'
|
|
12
|
-
import { uniqBy } from 'lodash-es'
|
|
13
|
-
import Password from '../../common/password'
|
|
14
|
-
import Upload from '../../common/upload'
|
|
15
|
-
|
|
16
|
-
const { TextArea } = Input
|
|
17
|
-
const FormItem = Form.Item
|
|
18
|
-
const e = window.translate
|
|
19
|
-
|
|
20
|
-
export default function renderAuth (props) {
|
|
21
|
-
const {
|
|
22
|
-
store,
|
|
23
|
-
form,
|
|
24
|
-
authType,
|
|
25
|
-
formItemName = 'password',
|
|
26
|
-
profileFilter = (d) => d
|
|
27
|
-
} = props
|
|
28
|
-
const beforeUpload = async (file) => {
|
|
29
|
-
const filePath = file.filePath
|
|
30
|
-
const privateKey = await window.fs.readFile(filePath)
|
|
31
|
-
form.setFieldsValue({
|
|
32
|
-
privateKey
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
if (authType === 'password') {
|
|
36
|
-
const opts = {
|
|
37
|
-
options: uniqBy(
|
|
38
|
-
store.bookmarks
|
|
39
|
-
.filter(d => d.password),
|
|
40
|
-
(d) => d.password
|
|
41
|
-
)
|
|
42
|
-
.map(d => ({
|
|
43
|
-
label: `${d.title ? `(${d.title})` : ''}${d.username || ''}:${d.host}-******`,
|
|
44
|
-
value: d.password
|
|
45
|
-
})),
|
|
46
|
-
placeholder: e('password'),
|
|
47
|
-
allowClear: false
|
|
48
|
-
}
|
|
49
|
-
return (
|
|
50
|
-
<FormItem
|
|
51
|
-
{...formItemLayout}
|
|
52
|
-
label={e('password')}
|
|
53
|
-
name={formItemName}
|
|
54
|
-
hasFeedback
|
|
55
|
-
rules={[{
|
|
56
|
-
max: 1024, message: '1024 chars max'
|
|
57
|
-
}]}
|
|
58
|
-
>
|
|
59
|
-
<AutoComplete {...opts}>
|
|
60
|
-
<Password />
|
|
61
|
-
</AutoComplete>
|
|
62
|
-
</FormItem>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
if (authType === 'profiles') {
|
|
66
|
-
const opts = {
|
|
67
|
-
options: store.profiles
|
|
68
|
-
.filter(profileFilter)
|
|
69
|
-
.map(d => ({ label: d.name, value: d.id })),
|
|
70
|
-
placeholder: e('profiles'),
|
|
71
|
-
allowClear: true
|
|
72
|
-
}
|
|
73
|
-
return (
|
|
74
|
-
<FormItem
|
|
75
|
-
{...formItemLayout}
|
|
76
|
-
label={e('profiles')}
|
|
77
|
-
name='profile'
|
|
78
|
-
hasFeedback
|
|
79
|
-
>
|
|
80
|
-
<Select {...opts} />
|
|
81
|
-
</FormItem>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
return [
|
|
85
|
-
<FormItem
|
|
86
|
-
{...formItemLayout}
|
|
87
|
-
label={e('privateKey')}
|
|
88
|
-
hasFeedback
|
|
89
|
-
key='privateKey'
|
|
90
|
-
className='mg1b'
|
|
91
|
-
rules={[{
|
|
92
|
-
max: 13000, message: '13000 chars max'
|
|
93
|
-
}]}
|
|
94
|
-
>
|
|
95
|
-
<FormItem noStyle name='privateKey'>
|
|
96
|
-
<TextArea placeholder={e('privateKeyDesc')} autoSize={{ minRows: 1 }} />
|
|
97
|
-
</FormItem>
|
|
98
|
-
<Upload beforeUpload={beforeUpload} fileList={[]}>
|
|
99
|
-
<Button type='dashed' className='mg2b mg1t'>
|
|
100
|
-
{e('importFromFile')}
|
|
101
|
-
</Button>
|
|
102
|
-
</Upload>
|
|
103
|
-
</FormItem>,
|
|
104
|
-
<FormItem
|
|
105
|
-
key='passphrase'
|
|
106
|
-
{...formItemLayout}
|
|
107
|
-
label={e('passphrase')}
|
|
108
|
-
name='passphrase'
|
|
109
|
-
hasFeedback
|
|
110
|
-
rules={[{
|
|
111
|
-
max: 1024, message: '1024 chars max'
|
|
112
|
-
}]}
|
|
113
|
-
>
|
|
114
|
-
<Password placeholder={e('passphraseDesc')} />
|
|
115
|
-
</FormItem>
|
|
116
|
-
]
|
|
117
|
-
}
|
|
1
|
+
export { default } from './ssh-auth-selector'
|
|
@@ -239,6 +239,13 @@ export const commonFields = {
|
|
|
239
239
|
type: 'runScripts',
|
|
240
240
|
name: 'runScripts',
|
|
241
241
|
label: ''
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
enableTerminalImage: {
|
|
245
|
+
type: 'switch',
|
|
246
|
+
name: 'enableTerminalImage',
|
|
247
|
+
label: () => e('enableTerminalImage'),
|
|
248
|
+
valuePropName: 'checked'
|
|
242
249
|
}
|
|
243
250
|
}
|
|
244
251
|
|
|
@@ -272,6 +279,7 @@ export const sshSettings = [
|
|
|
272
279
|
label: () => e('ignoreKeyboardInteractive'),
|
|
273
280
|
valuePropName: 'checked'
|
|
274
281
|
},
|
|
282
|
+
commonFields.enableTerminalImage,
|
|
275
283
|
...terminalSettings.slice(0, -1), // All except terminalBackground
|
|
276
284
|
commonFields.x11,
|
|
277
285
|
commonFields.terminalBackground
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formItemLayout } from '../../../common/form-layout.js'
|
|
2
|
-
import { terminalLocalType
|
|
2
|
+
import { terminalLocalType } from '../../../common/constants.js'
|
|
3
3
|
import {
|
|
4
4
|
createBaseInitValues,
|
|
5
5
|
getTerminalDefaults,
|
|
@@ -31,7 +31,8 @@ const localConfig = {
|
|
|
31
31
|
commonFields.category,
|
|
32
32
|
commonFields.colorTitle,
|
|
33
33
|
commonFields.description,
|
|
34
|
-
|
|
34
|
+
commonFields.enableTerminalImage,
|
|
35
|
+
commonFields.runScripts,
|
|
35
36
|
{ type: 'input', name: 'type', label: 'type', hidden: true }
|
|
36
37
|
]
|
|
37
38
|
},
|
|
@@ -39,54 +40,12 @@ const localConfig = {
|
|
|
39
40
|
key: 'settings',
|
|
40
41
|
label: e('settings'),
|
|
41
42
|
fields: [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
type: 'autocomplete',
|
|
50
|
-
name: 'term',
|
|
51
|
-
label: () => e('terminalType'),
|
|
52
|
-
rules: [{ required: true, message: 'terminal type required' }],
|
|
53
|
-
options: terminalTypes.map(t => ({ label: t, value: t }))
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
type: 'switch',
|
|
57
|
-
name: 'displayRaw',
|
|
58
|
-
label: () => e('displayRaw'),
|
|
59
|
-
valuePropName: 'checked'
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
type: 'input',
|
|
63
|
-
name: 'fontFamily',
|
|
64
|
-
label: () => e('fontFamily'),
|
|
65
|
-
rules: [{ max: 130, message: '130 chars max' }],
|
|
66
|
-
props: { placeholder: defaultSettings.fontFamily }
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
type: 'number',
|
|
70
|
-
name: 'fontSize',
|
|
71
|
-
label: () => e('fontSize'),
|
|
72
|
-
props: {
|
|
73
|
-
min: 9,
|
|
74
|
-
max: 65535,
|
|
75
|
-
step: 1,
|
|
76
|
-
placeholder: defaultSettings.fontSize
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
type: 'number',
|
|
81
|
-
name: 'keepaliveInterval',
|
|
82
|
-
label: () => e('keepaliveIntervalDesc'),
|
|
83
|
-
props: {
|
|
84
|
-
min: 0,
|
|
85
|
-
max: 20000000,
|
|
86
|
-
step: 1000
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
{ type: 'terminalBackground', name: 'terminalBackground', label: () => e('terminalBackgroundImage') },
|
|
43
|
+
commonFields.terminalType,
|
|
44
|
+
commonFields.displayRaw,
|
|
45
|
+
commonFields.fontFamily,
|
|
46
|
+
commonFields.fontSize,
|
|
47
|
+
commonFields.keepaliveInterval,
|
|
48
|
+
commonFields.terminalBackground,
|
|
90
49
|
// Exec settings - stored as flat properties on bookmark
|
|
91
50
|
{ type: 'execSettings' }
|
|
92
51
|
]
|
|
@@ -95,7 +54,7 @@ const localConfig = {
|
|
|
95
54
|
key: 'quickCommands',
|
|
96
55
|
label: e('quickCommands'),
|
|
97
56
|
fields: [
|
|
98
|
-
|
|
57
|
+
commonFields.quickCommands
|
|
99
58
|
]
|
|
100
59
|
}
|
|
101
60
|
]
|
|
@@ -29,7 +29,7 @@ import fs from '../../common/fs'
|
|
|
29
29
|
import ListTable from './list-table-ui'
|
|
30
30
|
import deepCopy from 'json-deep-copy'
|
|
31
31
|
import isValidPath from '../../common/is-valid-path'
|
|
32
|
-
import { LoadingOutlined } from '@ant-design/icons'
|
|
32
|
+
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
33
33
|
import * as owner from './owner-list'
|
|
34
34
|
import AddressBar from './address-bar'
|
|
35
35
|
import getProxy from '../../common/get-proxy'
|
|
@@ -956,6 +956,20 @@ export default class Sftp extends Component {
|
|
|
956
956
|
}, () => this[`${type}List`](undefined, undefined, oldPath))
|
|
957
957
|
}
|
|
958
958
|
|
|
959
|
+
handleReloadRemoteSftp = async () => {
|
|
960
|
+
if (this.sftp) {
|
|
961
|
+
this.sftp.destroy()
|
|
962
|
+
this.sftp = null
|
|
963
|
+
}
|
|
964
|
+
this.setState({
|
|
965
|
+
remoteLoading: true,
|
|
966
|
+
remote: [],
|
|
967
|
+
remoteFileTree: new Map()
|
|
968
|
+
}, () => {
|
|
969
|
+
this.initRemoteAll()
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
|
|
959
973
|
parsePath = (type, pth) => {
|
|
960
974
|
const reg = /^%([^%]+)%/
|
|
961
975
|
if (!reg.test(pth)) {
|
|
@@ -1152,6 +1166,25 @@ export default class Sftp extends Component {
|
|
|
1152
1166
|
)
|
|
1153
1167
|
}
|
|
1154
1168
|
|
|
1169
|
+
renderSftpPanelTitle (type, username, host) {
|
|
1170
|
+
if (type === typeMap.remote) {
|
|
1171
|
+
return (
|
|
1172
|
+
<div className='sftp-panel-title pd1t pd1b pd1x alignright'>
|
|
1173
|
+
<ReloadOutlined
|
|
1174
|
+
className='mg1r pointer'
|
|
1175
|
+
onClick={this.handleReloadRemoteSftp}
|
|
1176
|
+
/>
|
|
1177
|
+
{e('remote')}: {username}@{host}
|
|
1178
|
+
</div>
|
|
1179
|
+
)
|
|
1180
|
+
}
|
|
1181
|
+
return (
|
|
1182
|
+
<div className='sftp-panel-title pd1t pd1b pd1x'>
|
|
1183
|
+
{e('local')}
|
|
1184
|
+
</div>
|
|
1185
|
+
)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1155
1188
|
renderSection (type, style, width) {
|
|
1156
1189
|
const {
|
|
1157
1190
|
id
|
|
@@ -1222,17 +1255,7 @@ export default class Sftp extends Component {
|
|
|
1222
1255
|
<Spin spinning={loading}>
|
|
1223
1256
|
<div className='pd1 sftp-panel'>
|
|
1224
1257
|
{
|
|
1225
|
-
type
|
|
1226
|
-
? (
|
|
1227
|
-
<div className='sftp-panel-title pd1t pd1b pd1x alignright'>
|
|
1228
|
-
{e('remote')}: {username}@{host}
|
|
1229
|
-
</div>
|
|
1230
|
-
)
|
|
1231
|
-
: (
|
|
1232
|
-
<div className='sftp-panel-title pd1t pd1b pd1x'>
|
|
1233
|
-
{e('local')}
|
|
1234
|
-
</div>
|
|
1235
|
-
)
|
|
1258
|
+
this.renderSftpPanelTitle(type, username, host)
|
|
1236
1259
|
}
|
|
1237
1260
|
<AddressBar
|
|
1238
1261
|
{...addrProps}
|
|
@@ -26,7 +26,7 @@ export default auto(function HistoryPanel (props) {
|
|
|
26
26
|
} = store
|
|
27
27
|
let arr = store.config.disableConnectionHistory ? [] : history
|
|
28
28
|
if (sortByFrequency) {
|
|
29
|
-
arr = arr.sort((a, b) => { return b.count - a.count })
|
|
29
|
+
arr = [...arr].sort((a, b) => { return b.count - a.count })
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const handleSortByFrequencyChange = (checked) => {
|
|
@@ -19,6 +19,19 @@ export default function AppDrag (props) {
|
|
|
19
19
|
return true
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (window.store.shouldSendWindowMove) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
document.addEventListener('mouseup', onMouseUp)
|
|
27
|
+
window.addEventListener('contextmenu', onMouseUp)
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
document.removeEventListener('mouseup', onMouseUp)
|
|
31
|
+
window.removeEventListener('contextmenu', onMouseUp)
|
|
32
|
+
}
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
22
35
|
function onMouseDown (e) {
|
|
23
36
|
// e.stopPropagation()
|
|
24
37
|
if (canOperate(e)) {
|
|
@@ -48,18 +61,6 @@ export default function AppDrag (props) {
|
|
|
48
61
|
window.pre.runGlobalAsync('maximize')
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
|
-
if (!window.store.shouldSendWindowMove) {
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
// Listen for mouseup at document level to catch mouseup outside window
|
|
54
|
-
document.addEventListener('mouseup', onMouseUp)
|
|
55
|
-
window.addEventListener('contextmenu', onMouseUp)
|
|
56
|
-
|
|
57
|
-
return () => {
|
|
58
|
-
document.removeEventListener('mouseup', onMouseUp)
|
|
59
|
-
window.removeEventListener('contextmenu', onMouseUp)
|
|
60
|
-
}
|
|
61
|
-
}, [])
|
|
62
|
-
}
|
|
63
64
|
const props0 = {
|
|
64
65
|
className: 'app-drag',
|
|
65
66
|
onDoubleClick
|
|
@@ -58,7 +58,7 @@ export class KeywordHighlighterAddon {
|
|
|
58
58
|
const ESC = String.fromCharCode(27) // \x1b
|
|
59
59
|
const BEL = String.fromCharCode(7) // \x07
|
|
60
60
|
// eslint-disable-next-line no-control-regex
|
|
61
|
-
const ansiPattern = new RegExp('(' + ESC + '\\][^' + BEL + ESC + ']*(?:' + BEL + '|' + ESC + '\\\\)|' + ESC + '\\[[0-9;]*[A-Za-z])', 'g')
|
|
61
|
+
const ansiPattern = new RegExp('(' + ESC + '\\][^' + BEL + ESC + ']*(?:' + BEL + '|' + ESC + '\\\\)|' + ESC + '\\[\\??[0-9;]*[A-Za-z])', 'g')
|
|
62
62
|
|
|
63
63
|
const segments = []
|
|
64
64
|
let lastIndex = 0
|
|
@@ -833,25 +833,19 @@ class Term extends Component {
|
|
|
833
833
|
this.searchAddon.onDidChangeResults(this.onSearchResultsChange)
|
|
834
834
|
const Unicode11Addon = await loadUnicode11Addon()
|
|
835
835
|
const unicode11Addon = new Unicode11Addon()
|
|
836
|
-
if (config.enableSixel !== false) {
|
|
837
|
-
try {
|
|
838
|
-
const ImageAddon = await loadImageAddon()
|
|
839
|
-
this.imageAddon = new ImageAddon({
|
|
840
|
-
enableSizeReports: false,
|
|
841
|
-
sixelSupport: true,
|
|
842
|
-
iipSupport: false
|
|
843
|
-
})
|
|
844
|
-
term.loadAddon(this.imageAddon)
|
|
845
|
-
} catch (err) {
|
|
846
|
-
console.error('load sixel addon failed', err)
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
836
|
term.loadAddon(unicode11Addon)
|
|
850
837
|
term.loadAddon(ligtureAddon)
|
|
851
838
|
term.unicode.activeVersion = '11'
|
|
852
839
|
term.loadAddon(this.fitAddon)
|
|
853
840
|
term.loadAddon(this.searchAddon)
|
|
854
841
|
term.loadAddon(this.cmdAddon)
|
|
842
|
+
if (tab.enableTerminalImage) {
|
|
843
|
+
const ImageAddon = await loadImageAddon()
|
|
844
|
+
this.imageAddon = new ImageAddon({
|
|
845
|
+
pixelLimit: 33554432
|
|
846
|
+
})
|
|
847
|
+
term.loadAddon(this.imageAddon)
|
|
848
|
+
}
|
|
855
849
|
term.onData(this.onData)
|
|
856
850
|
this.term = term
|
|
857
851
|
term.onSelectionChange(this.onSelectionChange)
|
|
@@ -1401,6 +1395,7 @@ class Term extends Component {
|
|
|
1401
1395
|
totalLines: this.state.totalLines,
|
|
1402
1396
|
height
|
|
1403
1397
|
}
|
|
1398
|
+
const spinCls = loading ? 'loading-wrapper' : 'hide'
|
|
1404
1399
|
return (
|
|
1405
1400
|
<Dropdown {...dropdownProps}>
|
|
1406
1401
|
<div
|
|
@@ -1417,7 +1412,7 @@ class Term extends Component {
|
|
|
1417
1412
|
<RemoteFloatControl
|
|
1418
1413
|
isFullScreen={fullscreen}
|
|
1419
1414
|
/>
|
|
1420
|
-
<Spin className=
|
|
1415
|
+
<Spin className={spinCls} spinning={loading} />
|
|
1421
1416
|
</div>
|
|
1422
1417
|
</Dropdown>
|
|
1423
1418
|
)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// import { transferTypeMap } from '../../common/constants.js'
|
|
7
|
+
import { safeGetItem, safeSetItem } from '../../common/safe-local-storage.js'
|
|
7
8
|
import { getLocalFileInfo } from '../sftp/file-read.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -218,7 +219,7 @@ export class TransferClientBase {
|
|
|
218
219
|
*/
|
|
219
220
|
openSaveFolderSelect = async () => {
|
|
220
221
|
// Try to use last saved path
|
|
221
|
-
const lastPath = this.storageKey ?
|
|
222
|
+
const lastPath = this.storageKey ? safeGetItem(this.storageKey) : null
|
|
222
223
|
|
|
223
224
|
const savePaths = await window.api.openDialog({
|
|
224
225
|
title: 'Choose a folder to save file(s)',
|
|
@@ -238,9 +239,8 @@ export class TransferClientBase {
|
|
|
238
239
|
return null
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
// Save for next time
|
|
242
242
|
if (this.storageKey) {
|
|
243
|
-
|
|
243
|
+
safeSetItem(this.storageKey, savePaths[0])
|
|
244
244
|
}
|
|
245
245
|
return savePaths[0]
|
|
246
246
|
}
|
|
@@ -4,19 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react'
|
|
6
6
|
import { Button, Input, Space } from 'antd'
|
|
7
|
+
import { safeGetItem, safeSetItem } from '../../common/safe-local-storage.js'
|
|
7
8
|
|
|
8
9
|
const LS_KEY = 'customEditorCommand'
|
|
9
10
|
const e = window.translate
|
|
10
11
|
|
|
11
12
|
export default function EditWithCustomEditor ({ loading, editWithCustom }) {
|
|
12
13
|
const [editorCommand, setEditorCommand] = useState(
|
|
13
|
-
() =>
|
|
14
|
+
() => safeGetItem(LS_KEY) || ''
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
function handleChange (ev) {
|
|
17
18
|
const val = ev.target.value
|
|
18
19
|
setEditorCommand(val)
|
|
19
|
-
|
|
20
|
+
safeSetItem(LS_KEY, val)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function handleClick () {
|
|
@@ -31,7 +31,7 @@ export default function SimpleEditor (props) {
|
|
|
31
31
|
// Set selection range to select the matched text
|
|
32
32
|
textarea.setSelectionRange(match.start, match.end)
|
|
33
33
|
|
|
34
|
-
// Focus the textarea when explicitly navigating
|
|
34
|
+
// Focus the textarea when explicitly navigating to show highlight
|
|
35
35
|
textarea.focus()
|
|
36
36
|
|
|
37
37
|
// Scroll to the selection position
|
|
@@ -58,8 +58,6 @@ export default function SimpleEditor (props) {
|
|
|
58
58
|
|
|
59
59
|
// Auto-search when keyword changes (but not when text is being edited)
|
|
60
60
|
useEffect(() => {
|
|
61
|
-
// Set navigating to true so first match is highlighted when searching
|
|
62
|
-
setIsNavigating(true)
|
|
63
61
|
findMatches()
|
|
64
62
|
}, [searchKeyword])
|
|
65
63
|
|
|
@@ -99,10 +99,10 @@ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInsta
|
|
|
99
99
|
}
|
|
100
100
|
return (
|
|
101
101
|
<Alert
|
|
102
|
-
|
|
102
|
+
title='Downloading package may take some time on first use...'
|
|
103
103
|
type='warning'
|
|
104
104
|
showIcon
|
|
105
|
-
className='
|
|
105
|
+
className='mg1t'
|
|
106
106
|
/>
|
|
107
107
|
)
|
|
108
108
|
}
|
|
@@ -123,7 +123,7 @@ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInsta
|
|
|
123
123
|
</h4>
|
|
124
124
|
<p>{info.description}</p>
|
|
125
125
|
</div>
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
<Form
|
|
128
128
|
form={form}
|
|
129
129
|
onFinish={handleSubmit}
|
|
@@ -144,6 +144,7 @@ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInsta
|
|
|
144
144
|
{txt}
|
|
145
145
|
</Button>
|
|
146
146
|
</Tooltip>
|
|
147
|
+
{renderWarn()}
|
|
147
148
|
</Form.Item>
|
|
148
149
|
</Form>
|
|
149
150
|
</div>
|
|
@@ -54,7 +54,7 @@ export default () => {
|
|
|
54
54
|
lastDataUpdateTime: 0,
|
|
55
55
|
tabs: [],
|
|
56
56
|
activeTabId: '',
|
|
57
|
-
history: ls.
|
|
57
|
+
history: ls.safeGetItemJSON('history', []),
|
|
58
58
|
sshConfigs: [],
|
|
59
59
|
bookmarks: [],
|
|
60
60
|
bookmarksMap: new Map(),
|
|
@@ -77,7 +77,7 @@ export default () => {
|
|
|
77
77
|
// terminalCommandHistory: Map<cmdString, {count: Number, lastUseTime: DateString}>
|
|
78
78
|
// Load from localStorage and migrate from old format (Set of strings) if needed
|
|
79
79
|
terminalCommandHistory: (() => {
|
|
80
|
-
const savedData = ls.
|
|
80
|
+
const savedData = ls.safeGetItemJSON(cmdHistoryKey, [])
|
|
81
81
|
const map = new Map()
|
|
82
82
|
if (Array.isArray(savedData)) {
|
|
83
83
|
// Check if old format (array of strings) or new format (array of objects)
|
|
@@ -111,7 +111,7 @@ export default () => {
|
|
|
111
111
|
|
|
112
112
|
// batch input selected tab ids
|
|
113
113
|
_batchInputSelectedTabIds: new Set(),
|
|
114
|
-
aiChatHistory: ls.
|
|
114
|
+
aiChatHistory: ls.safeGetItemJSON(aiChatHistoryKey, []),
|
|
115
115
|
|
|
116
116
|
// sftp
|
|
117
117
|
fileOperation: fileOperationsMap.cp, // cp or mv
|
package/client/store/sync.js
CHANGED
package/client/store/watch.js
CHANGED
|
@@ -135,12 +135,12 @@ export default store => {
|
|
|
135
135
|
}).start()
|
|
136
136
|
|
|
137
137
|
autoRun(() => {
|
|
138
|
-
ls.
|
|
138
|
+
ls.safeSetItemJSON('history', store.history)
|
|
139
139
|
return store.history
|
|
140
140
|
}).start()
|
|
141
141
|
|
|
142
142
|
autoRun(() => {
|
|
143
|
-
ls.
|
|
143
|
+
ls.safeSetItemJSON(aiChatHistoryKey, store.aiChatHistory)
|
|
144
144
|
return store.aiChatHistory
|
|
145
145
|
}).start()
|
|
146
146
|
|
|
@@ -152,7 +152,7 @@ export default store => {
|
|
|
152
152
|
count: info.count,
|
|
153
153
|
lastUseTime: info.lastUseTime
|
|
154
154
|
}))
|
|
155
|
-
ls.
|
|
155
|
+
ls.safeSetItemJSON(cmdHistoryKey, data)
|
|
156
156
|
return store.terminalCommandHistory
|
|
157
157
|
}).start()
|
|
158
158
|
|