@electerm/electerm-react 3.15.126 → 3.15.138
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/constants.js +1 -0
- package/client/common/parse-quick-connect.js +3 -2
- package/client/components/ai/agent.js +14 -5
- package/client/components/ai/ai-chat-history-item.jsx +55 -6
- package/client/components/ai/ai-chat-session.jsx +51 -0
- package/client/components/ai/ai-chat-sessions.jsx +59 -0
- package/client/components/ai/ai-chat.jsx +81 -21
- package/client/components/ai/ai-config.jsx +8 -5
- package/client/components/ai/ai.styl +62 -9
- package/client/components/main/main.jsx +3 -1
- package/client/components/quick-commands/qm.styl +33 -0
- package/client/components/quick-commands/quick-command-item.jsx +82 -9
- package/client/components/quick-commands/quick-commands-box.jsx +6 -1
- package/client/components/quick-commands/quick-commands-list-form.jsx +12 -0
- package/client/components/quick-commands/quick-commands-list.jsx +2 -1
- package/client/components/shortcuts/shortcuts-defaults.js +1 -1
- package/client/components/terminal/attach-addon-custom.js +7 -1
- package/client/components/terminal/terminal-command-dropdown.jsx +22 -7
- package/client/components/terminal/terminal.jsx +32 -5
- package/client/store/common.js +162 -1
- package/client/store/init-state.js +4 -1
- package/client/store/load-data.js +46 -6
- package/package.json +1 -1
|
@@ -256,6 +256,7 @@ export const regexHelpLink = 'https://github.com/electerm/electerm/wiki/Terminal
|
|
|
256
256
|
export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/wiki/Connection-Hopping-Behavior-Change-in-electerm-since-v1.50.65'
|
|
257
257
|
export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
|
|
258
258
|
export const aiChatModeLsKey = 'ai-chat-mode'
|
|
259
|
+
export const lastAiChatSessionIdKey = 'last-ai-chat-session-id'
|
|
259
260
|
export const modals = {
|
|
260
261
|
hide: 0,
|
|
261
262
|
setting: 1
|
|
@@ -53,6 +53,7 @@ const TYPE_DEFAULT_VALUES = {
|
|
|
53
53
|
enableSsh: true,
|
|
54
54
|
enableSftp: true,
|
|
55
55
|
useSshAgent: true,
|
|
56
|
+
authType: 'password',
|
|
56
57
|
term: 'xterm-256color',
|
|
57
58
|
encode: 'utf-8',
|
|
58
59
|
envLang: 'en_US.UTF-8'
|
|
@@ -137,8 +138,8 @@ function parseQuickConnect (str) {
|
|
|
137
138
|
// Shortcut format - default to SSH
|
|
138
139
|
// Match user@host or user@host:port or just host or host:port
|
|
139
140
|
// Use last colon to determine port for host:port format
|
|
140
|
-
if (/^[\w.-]
|
|
141
|
-
// user@host or user@host:port
|
|
141
|
+
if (/^[\w.-]+(?::[^@]+)?@[\w.-]+/.test(input)) {
|
|
142
|
+
// user@host, user:password@host, or user@host:port
|
|
142
143
|
protocol = 'ssh'
|
|
143
144
|
connectionString = input
|
|
144
145
|
} else if (/^[\w.-]+:.*:[\d]+$/.test(input)) {
|
|
@@ -46,13 +46,22 @@ async function callBackendAIchatWithTools (messages, config) {
|
|
|
46
46
|
)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function runAgentLoop (chatEntry, config, abortRef, setIsStreaming) {
|
|
49
|
+
export async function runAgentLoop (chatEntry, config, abortRef, setIsStreaming, conversationMessages = null) {
|
|
50
50
|
window.store.agentRunning = true
|
|
51
51
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
let messages
|
|
53
|
+
if (conversationMessages && conversationMessages.length > 1) {
|
|
54
|
+
// Replace system message with agent system prompt, keep conversation history
|
|
55
|
+
messages = [
|
|
56
|
+
{ role: 'system', content: buildAgentSystemPrompt(config) },
|
|
57
|
+
...conversationMessages.filter(m => m.role !== 'system')
|
|
58
|
+
]
|
|
59
|
+
} else {
|
|
60
|
+
messages = [
|
|
61
|
+
{ role: 'system', content: buildAgentSystemPrompt(config) },
|
|
62
|
+
{ role: 'user', content: chatEntry.prompt }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
56
65
|
const toolCallsLog = []
|
|
57
66
|
let accumulatedContent = ''
|
|
58
67
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
|
2
2
|
import AIOutput from './ai-output'
|
|
3
3
|
import AIStopIcon from './ai-stop-icon'
|
|
4
4
|
import AgentToolCallCard from './agent-tool-call-card'
|
|
@@ -22,6 +22,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
22
22
|
const {
|
|
23
23
|
prompt,
|
|
24
24
|
sessionId,
|
|
25
|
+
chatSessionId,
|
|
25
26
|
nameAI,
|
|
26
27
|
modelAI,
|
|
27
28
|
roleAI,
|
|
@@ -29,6 +30,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
29
30
|
apiPathAI,
|
|
30
31
|
apiKeyAI,
|
|
31
32
|
proxyAI,
|
|
33
|
+
authHeaderNameAI,
|
|
32
34
|
languageAI,
|
|
33
35
|
mode,
|
|
34
36
|
toolCalls
|
|
@@ -43,6 +45,50 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
43
45
|
return roleAI + `;用[${lang}]回复`
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
const conversationMessages = useMemo(() => {
|
|
49
|
+
if (!chatSessionId) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
const sessionEntries = window.store.aiChatHistory
|
|
53
|
+
.filter(h => h.chatSessionId === chatSessionId && h.timestamp <= item.timestamp)
|
|
54
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
55
|
+
|
|
56
|
+
// Find the last compress entry
|
|
57
|
+
let lastCompressIndex = -1
|
|
58
|
+
for (let i = sessionEntries.length - 1; i >= 0; i--) {
|
|
59
|
+
if (sessionEntries[i].compressed) {
|
|
60
|
+
lastCompressIndex = i
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const messages = [
|
|
66
|
+
{ role: 'system', content: buildRole() }
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
// Start from the last compress entry (if any), skip older history
|
|
70
|
+
const startIndex = lastCompressIndex >= 0 ? lastCompressIndex : 0
|
|
71
|
+
for (let i = startIndex; i < sessionEntries.length; i++) {
|
|
72
|
+
const entry = sessionEntries[i]
|
|
73
|
+
if (entry.compressed) {
|
|
74
|
+
messages.push({
|
|
75
|
+
role: 'user',
|
|
76
|
+
content: `Here is a summary of our previous conversation for context:\n\n${entry.response}`
|
|
77
|
+
})
|
|
78
|
+
messages.push({
|
|
79
|
+
role: 'assistant',
|
|
80
|
+
content: 'Understood. I will use this context as we continue.'
|
|
81
|
+
})
|
|
82
|
+
} else {
|
|
83
|
+
messages.push({ role: 'user', content: entry.prompt })
|
|
84
|
+
if (entry.response && entry.id !== item.id) {
|
|
85
|
+
messages.push({ role: 'assistant', content: entry.response })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return messages
|
|
90
|
+
}, [chatSessionId, item.id, item.timestamp])
|
|
91
|
+
|
|
46
92
|
const pollStreamContent = useCallback(async (sid) => {
|
|
47
93
|
try {
|
|
48
94
|
const streamResponse = await window.pre.runGlobalAsync('getStreamContent', sid)
|
|
@@ -81,7 +127,9 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
81
127
|
apiPathAI,
|
|
82
128
|
apiKeyAI,
|
|
83
129
|
proxyAI,
|
|
84
|
-
true
|
|
130
|
+
true,
|
|
131
|
+
authHeaderNameAI,
|
|
132
|
+
conversationMessages
|
|
85
133
|
)
|
|
86
134
|
|
|
87
135
|
if (aiResponse && aiResponse.error) {
|
|
@@ -107,7 +155,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
107
155
|
window.store.removeAiHistory(item.id)
|
|
108
156
|
window.store.onError(error)
|
|
109
157
|
}
|
|
110
|
-
}, [prompt, modelAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, item.id, pollStreamContent])
|
|
158
|
+
}, [prompt, modelAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, authHeaderNameAI, item.id, pollStreamContent, conversationMessages])
|
|
111
159
|
|
|
112
160
|
const startAgentRequest = useCallback(async () => {
|
|
113
161
|
abortRef.current = false
|
|
@@ -118,10 +166,11 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
118
166
|
apiPathAI,
|
|
119
167
|
apiKeyAI,
|
|
120
168
|
proxyAI,
|
|
121
|
-
languageAI
|
|
169
|
+
languageAI,
|
|
170
|
+
authHeaderNameAI
|
|
122
171
|
}
|
|
123
|
-
await runAgentLoop(item, config, abortRef, setIsStreaming)
|
|
124
|
-
}, [modelAI, roleAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, languageAI, item.id])
|
|
172
|
+
await runAgentLoop(item, config, abortRef, setIsStreaming, conversationMessages)
|
|
173
|
+
}, [modelAI, roleAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, languageAI, authHeaderNameAI, item.id, conversationMessages])
|
|
125
174
|
|
|
126
175
|
useEffect(() => {
|
|
127
176
|
if (item.pending) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Flex, Button, Popconfirm } from 'antd'
|
|
2
|
+
import { DeleteOutlined, MessageOutlined } from '@ant-design/icons'
|
|
3
|
+
|
|
4
|
+
export default function AiChatSession ({
|
|
5
|
+
session,
|
|
6
|
+
isActive,
|
|
7
|
+
onLoadSession,
|
|
8
|
+
onDeleteSession
|
|
9
|
+
}) {
|
|
10
|
+
const time = new Date(session.timestamp).toLocaleString()
|
|
11
|
+
const preview = session.firstPrompt.length > 80
|
|
12
|
+
? session.firstPrompt.substring(0, 80) + '...'
|
|
13
|
+
: session.firstPrompt
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
key={session.sessionId}
|
|
17
|
+
className={`ai-chat-session-item${isActive ? ' active' : ''}`}
|
|
18
|
+
onClick={() => onLoadSession(session.sessionId)}
|
|
19
|
+
>
|
|
20
|
+
<Flex vertical className='ai-chat-session-content'>
|
|
21
|
+
<Flex align='center' className='ai-chat-session-header'>
|
|
22
|
+
<MessageOutlined className='mg1r' />
|
|
23
|
+
<span className='ai-chat-session-preview'>{preview}</span>
|
|
24
|
+
</Flex>
|
|
25
|
+
<Flex justify='space-between' align='center' className='ai-chat-session-meta'>
|
|
26
|
+
<span className='ai-chat-session-time'>{time}</span>
|
|
27
|
+
<span className='ai-chat-session-count'>{session.messageCount} messages</span>
|
|
28
|
+
</Flex>
|
|
29
|
+
</Flex>
|
|
30
|
+
<Popconfirm
|
|
31
|
+
title='Delete this chat session?'
|
|
32
|
+
okText='Delete'
|
|
33
|
+
cancelText='Cancel'
|
|
34
|
+
onConfirm={(e) => {
|
|
35
|
+
e?.stopPropagation()
|
|
36
|
+
onDeleteSession(session.sessionId)
|
|
37
|
+
}}
|
|
38
|
+
onCancel={(e) => e?.stopPropagation()}
|
|
39
|
+
>
|
|
40
|
+
<Button
|
|
41
|
+
size='small'
|
|
42
|
+
type='text'
|
|
43
|
+
danger
|
|
44
|
+
icon={<DeleteOutlined />}
|
|
45
|
+
onClick={(e) => e.stopPropagation()}
|
|
46
|
+
className='ai-chat-session-delete'
|
|
47
|
+
/>
|
|
48
|
+
</Popconfirm>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Flex, Button, Popconfirm, Empty } from 'antd'
|
|
2
|
+
import { ClearOutlined } from '@ant-design/icons'
|
|
3
|
+
import AiChatSession from './ai-chat-session'
|
|
4
|
+
|
|
5
|
+
const e = window.translate
|
|
6
|
+
|
|
7
|
+
export default function AiChatSessions ({
|
|
8
|
+
sessions,
|
|
9
|
+
currentChatSessionId,
|
|
10
|
+
onLoadSession,
|
|
11
|
+
onDeleteSession,
|
|
12
|
+
onClearAll
|
|
13
|
+
}) {
|
|
14
|
+
if (!sessions || sessions.length === 0) {
|
|
15
|
+
return (
|
|
16
|
+
<Flex
|
|
17
|
+
vertical
|
|
18
|
+
align='center'
|
|
19
|
+
justify='center'
|
|
20
|
+
className='ai-chat-sessions-empty'
|
|
21
|
+
>
|
|
22
|
+
<Empty
|
|
23
|
+
description='No chat history yet'
|
|
24
|
+
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
25
|
+
/>
|
|
26
|
+
</Flex>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className='ai-chat-sessions-wrap'>
|
|
32
|
+
<Flex justify='end' className='ai-chat-sessions-toolbar'>
|
|
33
|
+
<Popconfirm
|
|
34
|
+
title='?'
|
|
35
|
+
okText={e('clear')}
|
|
36
|
+
cancelText={e('cancel')}
|
|
37
|
+
onConfirm={onClearAll}
|
|
38
|
+
>
|
|
39
|
+
<Button
|
|
40
|
+
size='small'
|
|
41
|
+
danger
|
|
42
|
+
icon={<ClearOutlined />}
|
|
43
|
+
>
|
|
44
|
+
{e('clear')}
|
|
45
|
+
</Button>
|
|
46
|
+
</Popconfirm>
|
|
47
|
+
</Flex>
|
|
48
|
+
{sessions.map(session => (
|
|
49
|
+
<AiChatSession
|
|
50
|
+
key={session.sessionId}
|
|
51
|
+
session={session}
|
|
52
|
+
isActive={session.sessionId === currentChatSessionId}
|
|
53
|
+
onLoadSession={onLoadSession}
|
|
54
|
+
onDeleteSession={onDeleteSession}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
-
import { Flex, Input,
|
|
2
|
+
import { Flex, Input, Segmented, Button } from 'antd'
|
|
3
3
|
import TabSelect from '../footer/tab-select'
|
|
4
4
|
import AiChatHistory from './ai-chat-history'
|
|
5
|
+
import AiChatSessions from './ai-chat-sessions'
|
|
5
6
|
import uid from '../../common/uid'
|
|
6
7
|
import { pick } from 'lodash-es'
|
|
7
8
|
import {
|
|
8
9
|
SettingOutlined,
|
|
9
10
|
SendOutlined,
|
|
10
|
-
|
|
11
|
+
PlusOutlined,
|
|
12
|
+
HistoryOutlined,
|
|
13
|
+
CompressOutlined
|
|
11
14
|
} from '@ant-design/icons'
|
|
12
15
|
import {
|
|
13
16
|
aiConfigWikiLink,
|
|
@@ -19,14 +22,27 @@ import { refsStatic } from '../common/ref'
|
|
|
19
22
|
import './ai.styl'
|
|
20
23
|
|
|
21
24
|
const { TextArea } = Input
|
|
22
|
-
const MAX_HISTORY =
|
|
25
|
+
const MAX_HISTORY = 500
|
|
23
26
|
|
|
24
27
|
export default function AIChat (props) {
|
|
25
28
|
const [prompt, setPrompt] = useState('')
|
|
29
|
+
const [compressing, setCompressing] = useState(false)
|
|
26
30
|
const [mode, setMode] = useState(() => getItem(aiChatModeLsKey) || 'ask')
|
|
27
31
|
const isAgent = mode === 'agent'
|
|
28
32
|
const submitDisabled = isAgent && props.agentRunning
|
|
29
33
|
|
|
34
|
+
const currentChatSessionId = props.currentChatSessionId || ''
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!currentChatSessionId && props.rightPanelTab === 'ai') {
|
|
38
|
+
window.store.startNewChat()
|
|
39
|
+
}
|
|
40
|
+
}, [currentChatSessionId, props.rightPanelTab])
|
|
41
|
+
|
|
42
|
+
const sessionHistory = (props.aiChatHistory || []).filter(
|
|
43
|
+
h => h.chatSessionId === currentChatSessionId
|
|
44
|
+
)
|
|
45
|
+
|
|
30
46
|
function handlePromptChange (e) {
|
|
31
47
|
setPrompt(e.target.value)
|
|
32
48
|
}
|
|
@@ -50,6 +66,7 @@ export default function AIChat (props) {
|
|
|
50
66
|
isStreaming: false,
|
|
51
67
|
pending: true,
|
|
52
68
|
sessionId: null,
|
|
69
|
+
chatSessionId: currentChatSessionId,
|
|
53
70
|
mode,
|
|
54
71
|
toolCalls: [],
|
|
55
72
|
...pick(props.config, [
|
|
@@ -60,7 +77,8 @@ export default function AIChat (props) {
|
|
|
60
77
|
'apiPathAI',
|
|
61
78
|
'apiKeyAI',
|
|
62
79
|
'proxyAI',
|
|
63
|
-
'languageAI'
|
|
80
|
+
'languageAI',
|
|
81
|
+
'authHeaderNameAI'
|
|
64
82
|
]),
|
|
65
83
|
timestamp: Date.now(),
|
|
66
84
|
id: chatId
|
|
@@ -72,12 +90,23 @@ export default function AIChat (props) {
|
|
|
72
90
|
if (window.store.aiChatHistory.length > MAX_HISTORY) {
|
|
73
91
|
window.store.aiChatHistory.splice(MAX_HISTORY)
|
|
74
92
|
}
|
|
75
|
-
}, [prompt, mode])
|
|
93
|
+
}, [prompt, mode, currentChatSessionId])
|
|
76
94
|
|
|
77
95
|
function renderHistory () {
|
|
96
|
+
if (props.showChatSessions) {
|
|
97
|
+
return (
|
|
98
|
+
<AiChatSessions
|
|
99
|
+
sessions={window.store.getChatSessions()}
|
|
100
|
+
currentChatSessionId={currentChatSessionId}
|
|
101
|
+
onLoadSession={(sid) => window.store.loadChatSession(sid)}
|
|
102
|
+
onDeleteSession={(sid) => window.store.deleteChatSession(sid)}
|
|
103
|
+
onClearAll={() => window.store.clearAllChatSessions()}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
78
107
|
return (
|
|
79
108
|
<AiChatHistory
|
|
80
|
-
history={
|
|
109
|
+
history={sessionHistory}
|
|
81
110
|
/>
|
|
82
111
|
)
|
|
83
112
|
}
|
|
@@ -86,8 +115,21 @@ export default function AIChat (props) {
|
|
|
86
115
|
window.store.toggleAIConfig()
|
|
87
116
|
}
|
|
88
117
|
|
|
89
|
-
function
|
|
90
|
-
window.store.
|
|
118
|
+
function handleNewChat () {
|
|
119
|
+
window.store.startNewChat()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function handleCompressSession () {
|
|
123
|
+
setCompressing(true)
|
|
124
|
+
try {
|
|
125
|
+
await window.store.compressChatSession(currentChatSessionId)
|
|
126
|
+
} finally {
|
|
127
|
+
setCompressing(false)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleShowHistory () {
|
|
132
|
+
window.store.toggleChatSessions()
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
function renderTabSelect () {
|
|
@@ -146,14 +188,43 @@ export default function AIChat (props) {
|
|
|
146
188
|
}
|
|
147
189
|
}
|
|
148
190
|
}
|
|
149
|
-
|
|
191
|
+
const e = window.translate
|
|
150
192
|
return (
|
|
151
193
|
<Flex vertical className='ai-chat-container'>
|
|
152
194
|
<Flex className='ai-chat-history' flex='auto'>
|
|
153
195
|
{renderHistory()}
|
|
154
196
|
</Flex>
|
|
155
197
|
|
|
156
|
-
<Flex className='ai-chat-input'>
|
|
198
|
+
<Flex vertical className='ai-chat-input'>
|
|
199
|
+
<Flex className='ai-chat-toolbar mg1b' align='left' gap={4}>
|
|
200
|
+
<Button
|
|
201
|
+
size='small'
|
|
202
|
+
icon={<PlusOutlined />}
|
|
203
|
+
onClick={handleNewChat}
|
|
204
|
+
className='mg1r'
|
|
205
|
+
>
|
|
206
|
+
{e('new')}
|
|
207
|
+
</Button>
|
|
208
|
+
{sessionHistory.length >= 2 && (
|
|
209
|
+
<Button
|
|
210
|
+
size='small'
|
|
211
|
+
icon={<CompressOutlined />}
|
|
212
|
+
onClick={handleCompressSession}
|
|
213
|
+
loading={compressing}
|
|
214
|
+
className='mg1r'
|
|
215
|
+
>
|
|
216
|
+
{e('compress')}
|
|
217
|
+
</Button>
|
|
218
|
+
)}
|
|
219
|
+
<Button
|
|
220
|
+
size='small'
|
|
221
|
+
icon={<HistoryOutlined />}
|
|
222
|
+
onClick={handleShowHistory}
|
|
223
|
+
type={props.showChatSessions ? 'primary' : 'default'}
|
|
224
|
+
>
|
|
225
|
+
{e('history')}
|
|
226
|
+
</Button>
|
|
227
|
+
</Flex>
|
|
157
228
|
<TextArea
|
|
158
229
|
value={prompt}
|
|
159
230
|
onChange={handlePromptChange}
|
|
@@ -175,17 +246,6 @@ export default function AIChat (props) {
|
|
|
175
246
|
onClick={toggleConfig}
|
|
176
247
|
className='mg1l pointer icon-hover toggle-ai-setting-icon'
|
|
177
248
|
/>
|
|
178
|
-
<Popconfirm
|
|
179
|
-
title={window.translate('clear') + ' AI ' + window.translate('history') + '?'}
|
|
180
|
-
okText={window.translate('ok')}
|
|
181
|
-
cancelText={window.translate('cancel')}
|
|
182
|
-
onConfirm={clearHistory}
|
|
183
|
-
>
|
|
184
|
-
<UnorderedListOutlined
|
|
185
|
-
className='mg2x pointer clear-ai-icon icon-hover'
|
|
186
|
-
title='Clear AI chat history'
|
|
187
|
-
/>
|
|
188
|
-
</Popconfirm>
|
|
189
249
|
<HelpIcon
|
|
190
250
|
link={aiConfigWikiLink}
|
|
191
251
|
/>
|
|
@@ -143,11 +143,14 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
143
143
|
return { label, title }
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function
|
|
146
|
+
function renderApiKeyLabel () {
|
|
147
147
|
if (baseURLAI === 'https://api.atlascloud.ai/v1') {
|
|
148
|
-
return <span>API
|
|
148
|
+
return <span className='bold'>API Key (<Link to='https://www.atlascloud.ai/?utm_source=electerm_app&utm_medium=link&utm_campaign=electerm'>get API key from atlascloud</Link>)</span>
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
if (baseURLAI === 'https://ai.electerm.org/api/ai') {
|
|
151
|
+
return <span className='bold'>API Key (<Link to='https://ai.electerm.org?utm=electerm'>get API key from ai.electerm.org(free)</Link>)</span>
|
|
152
|
+
}
|
|
153
|
+
return 'API Key'
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
if (!showAIConfig) {
|
|
@@ -184,7 +187,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
184
187
|
placeholder='e.g. DeepSeek Relay, Local Ollama (optional)'
|
|
185
188
|
/>
|
|
186
189
|
</Form.Item>
|
|
187
|
-
<Form.Item label=
|
|
190
|
+
<Form.Item label='API URL' required>
|
|
188
191
|
<Space.Compact className='width-100'>
|
|
189
192
|
<Form.Item
|
|
190
193
|
label='API URL'
|
|
@@ -226,7 +229,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
226
229
|
</Form.Item>
|
|
227
230
|
|
|
228
231
|
<Form.Item
|
|
229
|
-
label=
|
|
232
|
+
label={renderApiKeyLabel()}
|
|
230
233
|
name='apiKeyAI'
|
|
231
234
|
>
|
|
232
235
|
<Password placeholder='Enter your API key' />
|
|
@@ -67,15 +67,68 @@
|
|
|
67
67
|
opacity 0.4
|
|
68
68
|
cursor not-allowed
|
|
69
69
|
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
.ai-chat-sessions-wrap
|
|
71
|
+
width 100%
|
|
72
|
+
overflow-y auto
|
|
73
|
+
overflow-x hidden
|
|
74
|
+
padding 4px 0
|
|
75
|
+
|
|
76
|
+
.ai-chat-sessions-toolbar
|
|
77
|
+
padding 0 4px 8px
|
|
78
|
+
|
|
79
|
+
.ai-chat-session-item
|
|
80
|
+
display flex
|
|
81
|
+
align-items center
|
|
82
|
+
padding 8px 10px
|
|
83
|
+
margin-bottom 6px
|
|
84
|
+
border 1px solid var(--main-darker)
|
|
85
|
+
border-radius 4px
|
|
86
|
+
cursor pointer
|
|
87
|
+
transition all 0.2s
|
|
88
|
+
&:hover
|
|
89
|
+
border-color #1890ff
|
|
90
|
+
background var(--main-dark)
|
|
91
|
+
.ai-chat-session-delete
|
|
92
|
+
opacity 1
|
|
93
|
+
&.active
|
|
94
|
+
border-color #1890ff
|
|
95
|
+
background rgba(24, 144, 255, 0.06)
|
|
96
|
+
|
|
97
|
+
.ai-chat-session-content
|
|
98
|
+
flex 1
|
|
99
|
+
min-width 0
|
|
100
|
+
overflow hidden
|
|
101
|
+
|
|
102
|
+
.ai-chat-session-header
|
|
103
|
+
margin-bottom 4px
|
|
104
|
+
|
|
105
|
+
.ai-chat-session-preview
|
|
106
|
+
font-size 13px
|
|
107
|
+
font-weight 500
|
|
108
|
+
white-space nowrap
|
|
109
|
+
overflow hidden
|
|
110
|
+
text-overflow ellipsis
|
|
111
|
+
|
|
112
|
+
.ai-chat-session-meta
|
|
113
|
+
font-size 12px
|
|
114
|
+
color var(--text-color-2)
|
|
115
|
+
|
|
116
|
+
.ai-chat-session-time
|
|
117
|
+
flex-shrink 0
|
|
118
|
+
|
|
119
|
+
.ai-chat-session-count
|
|
120
|
+
flex-shrink 0
|
|
121
|
+
margin-left 8px
|
|
122
|
+
|
|
123
|
+
.ai-chat-session-delete
|
|
124
|
+
opacity 0
|
|
125
|
+
flex-shrink 0
|
|
126
|
+
margin-left 8px
|
|
127
|
+
transition opacity 0.2s
|
|
128
|
+
|
|
129
|
+
.ai-chat-sessions-empty
|
|
130
|
+
height 100%
|
|
131
|
+
min-height 200px
|
|
79
132
|
|
|
80
133
|
.ai-stop-icon-square
|
|
81
134
|
width 20px
|
|
@@ -235,7 +235,9 @@ export default auto(function Index (props) {
|
|
|
235
235
|
activeTabId: store.activeTabId,
|
|
236
236
|
showAIConfig: store.showAIConfig,
|
|
237
237
|
rightPanelTab,
|
|
238
|
-
agentRunning: store.agentRunning
|
|
238
|
+
agentRunning: store.agentRunning,
|
|
239
|
+
currentChatSessionId: store.currentChatSessionId,
|
|
240
|
+
showChatSessions: store.showChatSessions
|
|
239
241
|
}
|
|
240
242
|
const cmdSuggestionsProps = {
|
|
241
243
|
suggestions: store.terminalCommandSuggestions
|
|
@@ -33,10 +33,43 @@
|
|
|
33
33
|
border-top 2px solid var(--primary)
|
|
34
34
|
.qm-label-select
|
|
35
35
|
min-width 120px
|
|
36
|
+
.qm-name-input
|
|
37
|
+
max-width 160px
|
|
38
|
+
min-width 80px
|
|
36
39
|
|
|
37
40
|
.qm-flex
|
|
38
41
|
gap 8px
|
|
39
42
|
|
|
43
|
+
.qm-tooltip-content
|
|
44
|
+
max-width 360px
|
|
45
|
+
.qm-tooltip-title
|
|
46
|
+
font-weight 600
|
|
47
|
+
margin-bottom 4px
|
|
48
|
+
word-break break-word
|
|
49
|
+
.qm-tooltip-cmd-list
|
|
50
|
+
margin 0
|
|
51
|
+
padding 0
|
|
52
|
+
list-style none
|
|
53
|
+
.qm-tooltip-cmd-item
|
|
54
|
+
padding 4px 0
|
|
55
|
+
border-top 1px solid rgba(255,255,255,0.15)
|
|
56
|
+
&:first-child
|
|
57
|
+
border-top none
|
|
58
|
+
.qm-tooltip-label
|
|
59
|
+
opacity 0.65
|
|
60
|
+
margin-right 4px
|
|
61
|
+
.qm-tooltip-value
|
|
62
|
+
word-break break-word
|
|
63
|
+
code
|
|
64
|
+
font-family monospace
|
|
65
|
+
.qm-tooltip-meta
|
|
66
|
+
margin-top 4px
|
|
67
|
+
padding-top 4px
|
|
68
|
+
border-top 1px solid rgba(255,255,255,0.15)
|
|
69
|
+
.qm-tooltip-label
|
|
70
|
+
opacity 0.65
|
|
71
|
+
margin-right 4px
|
|
72
|
+
|
|
40
73
|
@media (max-width: 768px)
|
|
41
74
|
.qm-flex
|
|
42
75
|
flex-direction column
|
|
@@ -6,19 +6,86 @@ import {
|
|
|
6
6
|
PureComponent
|
|
7
7
|
} from 'react'
|
|
8
8
|
import {
|
|
9
|
-
Button
|
|
9
|
+
Button,
|
|
10
|
+
Tooltip
|
|
10
11
|
} from 'antd'
|
|
11
12
|
import classNames from 'classnames'
|
|
12
13
|
|
|
14
|
+
const e = window.translate
|
|
15
|
+
|
|
13
16
|
export default class QuickCommandsItem extends PureComponent {
|
|
14
|
-
handleSelect = (
|
|
17
|
+
handleSelect = () => {
|
|
15
18
|
this.props.onSelect(
|
|
16
19
|
this.props.item.id
|
|
17
20
|
)
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
renderTooltip () {
|
|
24
|
+
const { name, commands, shortcut, inputOnly } = this.props.item
|
|
25
|
+
return (
|
|
26
|
+
<div className='qm-tooltip-content'>
|
|
27
|
+
{
|
|
28
|
+
name && (
|
|
29
|
+
<div className='qm-tooltip-title'>{name}</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
{
|
|
33
|
+
commands && commands.length > 0 && (
|
|
34
|
+
<ul className='qm-tooltip-cmd-list'>
|
|
35
|
+
{
|
|
36
|
+
commands.map((c, i) => (
|
|
37
|
+
<li key={i} className='qm-tooltip-cmd-item'>
|
|
38
|
+
{
|
|
39
|
+
c.name && (
|
|
40
|
+
<div className='qm-tooltip-cmd-name'>
|
|
41
|
+
<span className='qm-tooltip-label'>{e('name')}:</span>
|
|
42
|
+
<span className='qm-tooltip-value'>{c.name}</span>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
<div className='qm-tooltip-cmd-text'>
|
|
47
|
+
<span className='qm-tooltip-label'>{e('quickCommand')}:</span>
|
|
48
|
+
<code className='qm-tooltip-value'>{c.command}</code>
|
|
49
|
+
</div>
|
|
50
|
+
{
|
|
51
|
+
c.delay && (
|
|
52
|
+
<div className='qm-tooltip-cmd-delay'>
|
|
53
|
+
<span className='qm-tooltip-label'>{e('delay')}:</span>
|
|
54
|
+
<span className='qm-tooltip-value'>{c.delay}ms</span>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
</li>
|
|
59
|
+
))
|
|
60
|
+
}
|
|
61
|
+
</ul>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
{
|
|
65
|
+
(shortcut || inputOnly) && (
|
|
66
|
+
<div className='qm-tooltip-meta'>
|
|
67
|
+
{
|
|
68
|
+
shortcut && (
|
|
69
|
+
<div>
|
|
70
|
+
<span className='qm-tooltip-label'>{e('settingShortcuts')}:</span>
|
|
71
|
+
<span className='qm-tooltip-value'>{shortcut}</span>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
{
|
|
76
|
+
inputOnly && (
|
|
77
|
+
<div>[{e('inputOnly')}]</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
20
87
|
render () {
|
|
21
|
-
const { name, id
|
|
88
|
+
const { name, id } = this.props.item
|
|
22
89
|
const {
|
|
23
90
|
draggable,
|
|
24
91
|
handleDragOver,
|
|
@@ -32,7 +99,6 @@ export default class QuickCommandsItem extends PureComponent {
|
|
|
32
99
|
className: cls,
|
|
33
100
|
onClick: this.handleSelect,
|
|
34
101
|
'data-id': id,
|
|
35
|
-
title: shortcut,
|
|
36
102
|
draggable,
|
|
37
103
|
onDragOver: handleDragOver,
|
|
38
104
|
onDragStart: handleDragStart,
|
|
@@ -41,12 +107,19 @@ export default class QuickCommandsItem extends PureComponent {
|
|
|
41
107
|
onDrop: handleDrop
|
|
42
108
|
}
|
|
43
109
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
110
|
+
<Tooltip
|
|
111
|
+
title={this.renderTooltip()}
|
|
112
|
+
placement='top'
|
|
113
|
+
mouseEnterDelay={0.5}
|
|
114
|
+
overlayClassName='qm-tooltip-overlay'
|
|
47
115
|
>
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
<Button
|
|
117
|
+
key={id}
|
|
118
|
+
{...btnProps}
|
|
119
|
+
>
|
|
120
|
+
{name}
|
|
121
|
+
</Button>
|
|
122
|
+
</Tooltip>
|
|
50
123
|
)
|
|
51
124
|
}
|
|
52
125
|
}
|
|
@@ -144,7 +144,12 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
144
144
|
|
|
145
145
|
function filterArray (array, keyword, label) {
|
|
146
146
|
return array.filter(obj => {
|
|
147
|
-
const nameMatches = !keyword ||
|
|
147
|
+
const nameMatches = !keyword ||
|
|
148
|
+
obj.name.toLowerCase().includes(keyword) ||
|
|
149
|
+
(obj.commands || []).some(cmd =>
|
|
150
|
+
(cmd.command || '').toLowerCase().includes(keyword) ||
|
|
151
|
+
(cmd.name || '').toLowerCase().includes(keyword)
|
|
152
|
+
)
|
|
148
153
|
const labelMatches = !label || (obj.labels || []).includes(label)
|
|
149
154
|
return nameMatches && labelMatches
|
|
150
155
|
})
|
|
@@ -78,6 +78,18 @@ export default function renderQm (form) {
|
|
|
78
78
|
>
|
|
79
79
|
<HolderOutlined className='mg1r drag' />
|
|
80
80
|
|
|
81
|
+
<FormItem
|
|
82
|
+
label=''
|
|
83
|
+
name={[field.name, 'name']}
|
|
84
|
+
noStyle
|
|
85
|
+
rules={[{ max: 100, message: '60 chars max' }]}
|
|
86
|
+
>
|
|
87
|
+
<Input
|
|
88
|
+
placeholder={e('name')}
|
|
89
|
+
className='compact-input qm-name-input'
|
|
90
|
+
maxLength={100}
|
|
91
|
+
/>
|
|
92
|
+
</FormItem>
|
|
81
93
|
<Space.Addon>{e('delay')}</Space.Addon>
|
|
82
94
|
<FormItem
|
|
83
95
|
label=''
|
|
@@ -181,7 +181,8 @@ export default class QuickCommandsList extends List {
|
|
|
181
181
|
if (item.commands && Array.isArray(item.commands)) {
|
|
182
182
|
// Search in each command in the commands array
|
|
183
183
|
return n.includes(k) || item.commands.some(cmd =>
|
|
184
|
-
(cmd.command || '').toLowerCase().includes(k)
|
|
184
|
+
(cmd.command || '').toLowerCase().includes(k) ||
|
|
185
|
+
(cmd.name || '').toLowerCase().includes(k)
|
|
185
186
|
)
|
|
186
187
|
} else {
|
|
187
188
|
// Fallback to the old behavior for backward compatibility
|
|
@@ -224,7 +224,12 @@ export default class AttachAddonCustom {
|
|
|
224
224
|
|
|
225
225
|
if (typeof data === 'string') {
|
|
226
226
|
term?.parent?.notifyOnData()
|
|
227
|
-
|
|
227
|
+
term.write(data)
|
|
228
|
+
// Notify parent that the terminal buffer has been updated (echo received).
|
|
229
|
+
// This allows the suggestion dropdown to refresh after the buffer reflects
|
|
230
|
+
// the latest server-side state (e.g., after backspace echo).
|
|
231
|
+
term?.parent?.onTerminalWrite?.()
|
|
232
|
+
return
|
|
228
233
|
}
|
|
229
234
|
data = new Uint8Array(data)
|
|
230
235
|
const fileReader = new FileReader()
|
|
@@ -238,6 +243,7 @@ export default class AttachAddonCustom {
|
|
|
238
243
|
term?.parent?.notifyOnData()
|
|
239
244
|
const str = this.decoder.decode(data)
|
|
240
245
|
term?.write(str)
|
|
246
|
+
term?.parent?.onTerminalWrite?.()
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
sendToServer = (data) => {
|
|
@@ -2,7 +2,6 @@ import { Component } from 'manate/react/class-components'
|
|
|
2
2
|
import { refsStatic, refs } from '../common/ref'
|
|
3
3
|
import SuggestionItem from './cmd-item'
|
|
4
4
|
import { aiSuggestionsCache } from '../../common/cache'
|
|
5
|
-
import uid from '../../common/uid'
|
|
6
5
|
import classnames from 'classnames'
|
|
7
6
|
import {
|
|
8
7
|
LoadingOutlined
|
|
@@ -130,6 +129,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
130
129
|
} else {
|
|
131
130
|
position.top = top + cellHeight
|
|
132
131
|
}
|
|
132
|
+
this._suggestionsCache = null
|
|
133
133
|
this.setState({
|
|
134
134
|
showSuggestions: true,
|
|
135
135
|
cursorPosition: position,
|
|
@@ -188,6 +188,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
188
188
|
window.store.addCmdHistory(item.command, 'aiCmdHistory')
|
|
189
189
|
})
|
|
190
190
|
}
|
|
191
|
+
this._suggestionsCache = null
|
|
191
192
|
this.setState({
|
|
192
193
|
showSuggestions: false,
|
|
193
194
|
aiSuggestions: [],
|
|
@@ -261,7 +262,8 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
261
262
|
if (!uniqueCommands.has(command)) {
|
|
262
263
|
uniqueCommands.add(command)
|
|
263
264
|
res.push({
|
|
264
|
-
|
|
265
|
+
// Use stable key to avoid React re-mounting items on every render
|
|
266
|
+
id: type + ':' + command,
|
|
265
267
|
command,
|
|
266
268
|
type
|
|
267
269
|
})
|
|
@@ -277,7 +279,7 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
277
279
|
if (b.password && !seen.has(b.password)) {
|
|
278
280
|
seen.add(b.password)
|
|
279
281
|
res.push({
|
|
280
|
-
id:
|
|
282
|
+
id: 'PW:' + b.password,
|
|
281
283
|
command: b.password,
|
|
282
284
|
type: 'PW',
|
|
283
285
|
hint: [b.username, [b.host, b.port].filter(Boolean).join(':')].filter(Boolean).join('@')
|
|
@@ -287,28 +289,41 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
287
289
|
return this.state.reverse ? res.reverse() : res
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
_suggestionsCache = null
|
|
293
|
+
_suggestionsCacheKey = ''
|
|
294
|
+
|
|
290
295
|
getSuggestions = () => {
|
|
296
|
+
// Memoize: only recompute when cmd, reverse, aiSuggestions, or props change
|
|
297
|
+
const { cmd, reverse, aiSuggestions } = this.state
|
|
298
|
+
const { suggestions } = this.props
|
|
299
|
+
const cacheKey = cmd + '|' + reverse + '|' + (aiSuggestions?.length || 0) + '|' + (suggestions?.history?.length || 0) + '|' + (suggestions?.batch?.length || 0) + '|' + (suggestions?.quick?.length || 0)
|
|
300
|
+
if (this._suggestionsCache && this._suggestionsCacheKey === cacheKey) {
|
|
301
|
+
return this._suggestionsCache
|
|
302
|
+
}
|
|
291
303
|
const uniqueCommands = new Set()
|
|
292
304
|
const {
|
|
293
305
|
history = [],
|
|
294
306
|
batch = [],
|
|
295
307
|
quick = []
|
|
296
|
-
} =
|
|
308
|
+
} = suggestions || {}
|
|
297
309
|
const res = []
|
|
298
|
-
|
|
310
|
+
aiSuggestions
|
|
299
311
|
.forEach(item => {
|
|
300
312
|
if (!uniqueCommands.has(item.command)) {
|
|
301
313
|
uniqueCommands.add(item.command)
|
|
302
314
|
}
|
|
303
315
|
res.push({
|
|
304
|
-
id:
|
|
316
|
+
id: 'AI:' + item.command,
|
|
305
317
|
...item
|
|
306
318
|
})
|
|
307
319
|
})
|
|
308
320
|
this.processCommands(history, 'H', uniqueCommands, res)
|
|
309
321
|
this.processCommands(batch, 'B', uniqueCommands, res)
|
|
310
322
|
this.processCommands(quick, 'Q', uniqueCommands, res)
|
|
311
|
-
|
|
323
|
+
const finalRes = reverse ? res.reverse() : res
|
|
324
|
+
this._suggestionsCache = finalRes
|
|
325
|
+
this._suggestionsCacheKey = cacheKey
|
|
326
|
+
return finalRes
|
|
312
327
|
}
|
|
313
328
|
|
|
314
329
|
renderAIIcon () {
|
|
@@ -1081,18 +1081,45 @@ class Term extends Component {
|
|
|
1081
1081
|
return
|
|
1082
1082
|
}
|
|
1083
1083
|
if (this.props.config.showCmdSuggestions) {
|
|
1084
|
-
|
|
1085
|
-
if (data && d !== '\r' && d !== '\n') {
|
|
1086
|
-
const cursorPos = this.getCursorPosition()
|
|
1087
|
-
this.openSuggestions(cursorPos, data)
|
|
1088
|
-
} else {
|
|
1084
|
+
if (d === '\r' || d === '\n') {
|
|
1089
1085
|
this.closeSuggestions()
|
|
1086
|
+
return
|
|
1090
1087
|
}
|
|
1088
|
+
// Debounce the suggestion opening to avoid expensive work
|
|
1089
|
+
// (buffer read + getBoundingClientRect + React re-render) on every keystroke
|
|
1090
|
+
this._debouncedOpenSuggestions()
|
|
1091
1091
|
} else {
|
|
1092
1092
|
this.closeSuggestions()
|
|
1093
1093
|
}
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
|
+
_debouncedOpenSuggestions = debounce(function () {
|
|
1097
|
+
const data = this.getCurrentInput()
|
|
1098
|
+
if (!data) {
|
|
1099
|
+
this.closeSuggestions()
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
const cursorPos = this.getCursorPosition()
|
|
1103
|
+
this.openSuggestions(cursorPos, data)
|
|
1104
|
+
}, 80)
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Called by AttachAddonCustom after data is written to the terminal buffer.
|
|
1108
|
+
* This fires after server echo arrives, so getCurrentInput() reflects the
|
|
1109
|
+
* latest state. We trigger a debounced suggestion refresh so the dropdown
|
|
1110
|
+
* updates correctly after backspace, delete, and other edits that rely on
|
|
1111
|
+
* server-side echo to update the buffer.
|
|
1112
|
+
*/
|
|
1113
|
+
onTerminalWrite = () => {
|
|
1114
|
+
if (!this.props.config.showCmdSuggestions) {
|
|
1115
|
+
return
|
|
1116
|
+
}
|
|
1117
|
+
const suggestions = refsStatic.get('terminal-suggestions')
|
|
1118
|
+
if (suggestions?.state?.showSuggestions && !suggestions?.state?.passwordMode) {
|
|
1119
|
+
this._debouncedOpenSuggestions()
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1096
1123
|
loadRenderer = async (term, config) => {
|
|
1097
1124
|
// xterm 6.x: only the built-in DOM renderer and the WebGL addon exist
|
|
1098
1125
|
// (the canvas renderer addon was removed in 6.x). 'dom' = no addon loaded
|
package/client/store/common.js
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
rightSidebarWidthKey,
|
|
11
11
|
addPanelWidthLsKey,
|
|
12
12
|
dismissDelKeyTipLsKey,
|
|
13
|
-
connectionMap
|
|
13
|
+
connectionMap,
|
|
14
|
+
lastAiChatSessionIdKey
|
|
14
15
|
} from '../common/constants'
|
|
15
16
|
import * as ls from '../common/safe-local-storage'
|
|
16
17
|
import { refs, refsStatic } from '../components/common/ref'
|
|
@@ -256,6 +257,166 @@ export default Store => {
|
|
|
256
257
|
window.store.aiChatHistory.splice(index, 1)
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
Store.prototype.startNewChat = action(function () {
|
|
261
|
+
const { store } = window
|
|
262
|
+
store.currentChatSessionId = uid()
|
|
263
|
+
store.showChatSessions = false
|
|
264
|
+
window.localStorage.setItem(lastAiChatSessionIdKey, store.currentChatSessionId)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
Store.prototype.loadChatSession = action(function (sessionId) {
|
|
268
|
+
const { store } = window
|
|
269
|
+
store.currentChatSessionId = sessionId
|
|
270
|
+
store.showChatSessions = false
|
|
271
|
+
window.localStorage.setItem(lastAiChatSessionIdKey, sessionId)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
Store.prototype.deleteChatSession = action(function (sessionId) {
|
|
275
|
+
const { store } = window
|
|
276
|
+
const remaining = store.aiChatHistory.filter(d => d.chatSessionId !== sessionId)
|
|
277
|
+
store.aiChatHistory = remaining
|
|
278
|
+
if (store.currentChatSessionId === sessionId) {
|
|
279
|
+
store.startNewChat()
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
Store.prototype.clearAllChatSessions = action(function () {
|
|
284
|
+
const { store } = window
|
|
285
|
+
store.aiChatHistory = []
|
|
286
|
+
store.showChatSessions = false
|
|
287
|
+
store.startNewChat()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
Store.prototype.compressChatSession = async function (sessionId) {
|
|
291
|
+
const { store } = window
|
|
292
|
+
const sessionEntries = store.aiChatHistory
|
|
293
|
+
.filter(h => h.chatSessionId === sessionId)
|
|
294
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
295
|
+
|
|
296
|
+
// Find the last compress entry
|
|
297
|
+
let lastCompressIndex = -1
|
|
298
|
+
for (let i = sessionEntries.length - 1; i >= 0; i--) {
|
|
299
|
+
if (sessionEntries[i].compressed) {
|
|
300
|
+
lastCompressIndex = i
|
|
301
|
+
break
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Need at least 2 non-compress entries since the last compress
|
|
306
|
+
const entriesAfterCompress = lastCompressIndex >= 0
|
|
307
|
+
? sessionEntries.slice(lastCompressIndex + 1)
|
|
308
|
+
: sessionEntries
|
|
309
|
+
if (entriesAfterCompress.length < 2) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const firstEntry = sessionEntries[0]
|
|
314
|
+
const lang = firstEntry.languageAI || store.getLangName()
|
|
315
|
+
const messages = [
|
|
316
|
+
{ role: 'system', content: firstEntry.roleAI + `;用[${lang}]回复` }
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
// Start from the last compress entry to include its summary as context
|
|
320
|
+
const startIndex = lastCompressIndex >= 0 ? lastCompressIndex : 0
|
|
321
|
+
for (let i = startIndex; i < sessionEntries.length; i++) {
|
|
322
|
+
const entry = sessionEntries[i]
|
|
323
|
+
if (entry.compressed) {
|
|
324
|
+
messages.push({
|
|
325
|
+
role: 'user',
|
|
326
|
+
content: `Here is a summary of our previous conversation for context:\n\n${entry.response}`
|
|
327
|
+
})
|
|
328
|
+
messages.push({
|
|
329
|
+
role: 'assistant',
|
|
330
|
+
content: 'Understood. I will use this context as we continue.'
|
|
331
|
+
})
|
|
332
|
+
} else {
|
|
333
|
+
messages.push({ role: 'user', content: entry.prompt })
|
|
334
|
+
if (entry.response) {
|
|
335
|
+
messages.push({ role: 'assistant', content: entry.response })
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
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.'
|
|
341
|
+
messages.push({ role: 'user', content: summaryPrompt })
|
|
342
|
+
|
|
343
|
+
const aiResponse = await window.pre.runGlobalAsync(
|
|
344
|
+
'AIchat',
|
|
345
|
+
summaryPrompt,
|
|
346
|
+
firstEntry.modelAI,
|
|
347
|
+
firstEntry.roleAI,
|
|
348
|
+
firstEntry.baseURLAI,
|
|
349
|
+
firstEntry.apiPathAI,
|
|
350
|
+
firstEntry.apiKeyAI,
|
|
351
|
+
firstEntry.proxyAI,
|
|
352
|
+
false,
|
|
353
|
+
firstEntry.authHeaderNameAI,
|
|
354
|
+
messages
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if (aiResponse && aiResponse.error) {
|
|
358
|
+
return store.onError(new Error(aiResponse.error))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const summary = aiResponse.response || ''
|
|
362
|
+
const compressedEntry = {
|
|
363
|
+
id: uid(),
|
|
364
|
+
prompt: '*Compressed session summary*',
|
|
365
|
+
response: summary,
|
|
366
|
+
isStreaming: false,
|
|
367
|
+
pending: false,
|
|
368
|
+
sessionId: null,
|
|
369
|
+
chatSessionId: sessionId,
|
|
370
|
+
mode: firstEntry.mode,
|
|
371
|
+
toolCalls: [],
|
|
372
|
+
nameAI: firstEntry.nameAI,
|
|
373
|
+
modelAI: firstEntry.modelAI,
|
|
374
|
+
roleAI: firstEntry.roleAI,
|
|
375
|
+
baseURLAI: firstEntry.baseURLAI,
|
|
376
|
+
apiPathAI: firstEntry.apiPathAI,
|
|
377
|
+
apiKeyAI: firstEntry.apiKeyAI,
|
|
378
|
+
proxyAI: firstEntry.proxyAI,
|
|
379
|
+
languageAI: firstEntry.languageAI,
|
|
380
|
+
authHeaderNameAI: firstEntry.authHeaderNameAI,
|
|
381
|
+
timestamp: Date.now(),
|
|
382
|
+
compressed: true
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Append compress entry, preserve existing history
|
|
386
|
+
store.aiChatHistory.push(compressedEntry)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
Store.prototype.toggleChatSessions = action(function () {
|
|
390
|
+
const { store } = window
|
|
391
|
+
store.showChatSessions = !store.showChatSessions
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
Store.prototype.getChatSessions = function () {
|
|
395
|
+
const { aiChatHistory } = window.store
|
|
396
|
+
const sessionMap = new Map()
|
|
397
|
+
for (const entry of aiChatHistory) {
|
|
398
|
+
const sid = entry.chatSessionId
|
|
399
|
+
if (!sid) continue
|
|
400
|
+
if (!sessionMap.has(sid)) {
|
|
401
|
+
sessionMap.set(sid, {
|
|
402
|
+
sessionId: sid,
|
|
403
|
+
firstPrompt: entry.prompt || '',
|
|
404
|
+
timestamp: entry.timestamp,
|
|
405
|
+
messageCount: 1,
|
|
406
|
+
entries: [entry]
|
|
407
|
+
})
|
|
408
|
+
} else {
|
|
409
|
+
const session = sessionMap.get(sid)
|
|
410
|
+
session.messageCount++
|
|
411
|
+
session.entries.push(entry)
|
|
412
|
+
if (entry.timestamp > session.timestamp) {
|
|
413
|
+
session.timestamp = entry.timestamp
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return Array.from(sessionMap.values()).sort((a, b) => b.timestamp - a.timestamp)
|
|
418
|
+
}
|
|
419
|
+
|
|
259
420
|
Store.prototype.getLangName = function (
|
|
260
421
|
lang = window.store?.config.language || 'en_us'
|
|
261
422
|
) {
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
qmSortByFrequencyKey,
|
|
23
23
|
resolutionsLsKey,
|
|
24
24
|
syncServerDataKey,
|
|
25
|
-
splitMap
|
|
25
|
+
splitMap,
|
|
26
|
+
lastAiChatSessionIdKey
|
|
26
27
|
} from '../common/constants'
|
|
27
28
|
import * as ls from '../common/safe-local-storage'
|
|
28
29
|
import { exclude } from 'manate'
|
|
@@ -89,6 +90,8 @@ export default () => {
|
|
|
89
90
|
_batchInputSelectedTabIds: new Set(),
|
|
90
91
|
aiChatHistory: [],
|
|
91
92
|
agentRunning: false,
|
|
93
|
+
currentChatSessionId: window.localStorage.getItem(lastAiChatSessionIdKey) || '',
|
|
94
|
+
showChatSessions: false,
|
|
92
95
|
|
|
93
96
|
// sftp
|
|
94
97
|
fileOperation: fileOperationsMap.cp, // cp or mv
|
|
@@ -114,12 +114,52 @@ export async function addTabFromCommandLine (store, opts) {
|
|
|
114
114
|
conf.fromCmdLine
|
|
115
115
|
) {
|
|
116
116
|
store.ipcOpenTab(conf)
|
|
117
|
-
} else
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
} else {
|
|
118
|
+
// getHost didn't find a match, try parseQuickConnect for shortcut formats
|
|
119
|
+
// that getHost doesn't support (e.g., user:password@host)
|
|
120
|
+
let parsedFallback = null
|
|
121
|
+
if (argv && argv.length) {
|
|
122
|
+
for (const arg of argv) {
|
|
123
|
+
if (/^-/.test(arg)) continue
|
|
124
|
+
const result = parseQuickConnect(arg)
|
|
125
|
+
if (result && result.host) {
|
|
126
|
+
parsedFallback = result
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (parsedFallback) {
|
|
132
|
+
// Apply command-line options on top of parsed result
|
|
133
|
+
if (options.password) parsedFallback.password = options.password
|
|
134
|
+
if (options.passphrase) parsedFallback.passphrase = options.passphrase
|
|
135
|
+
if (options.user) parsedFallback.username = options.user
|
|
136
|
+
if (options.port && parseInt10(options.port)) parsedFallback.port = parseInt10(options.port)
|
|
137
|
+
if (options.title) parsedFallback.title = options.title
|
|
138
|
+
if (options.setEnv) parsedFallback.setEnv = options.setEnv
|
|
139
|
+
if (options.sftpOnly) parsedFallback.enableSsh = false
|
|
140
|
+
if (options.initFolder) parsedFallback.startDirectoryLocal = options.initFolder
|
|
141
|
+
if (options.opts) {
|
|
142
|
+
const optsParsed = safeParse(options.opts)
|
|
143
|
+
if (optsParsed !== options.opts) {
|
|
144
|
+
Object.assign(parsedFallback, optsParsed)
|
|
145
|
+
parsedFallback.fromCmdLine = true
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (options.tp) {
|
|
149
|
+
parsedFallback.type = options.tp
|
|
150
|
+
parsedFallback.fromCmdLine = true
|
|
151
|
+
}
|
|
152
|
+
if (options.privateKeyPath) {
|
|
153
|
+
parsedFallback.privateKey = await window.fs.readFile(options.privateKeyPath)
|
|
154
|
+
}
|
|
155
|
+
store.ipcOpenTab(parsedFallback)
|
|
156
|
+
} else if (
|
|
157
|
+
options.initFolder &&
|
|
158
|
+
!(store.config.onStartSessions || []).length &&
|
|
159
|
+
store.config.initDefaultTabOnStart
|
|
160
|
+
) {
|
|
161
|
+
window.initFolder = options.initFolder
|
|
162
|
+
}
|
|
123
163
|
}
|
|
124
164
|
if (options.batchOp) {
|
|
125
165
|
refsStatic.get('batch-op-runner')?.runBatchOpFromFile(options.batchOp)
|