@electerm/electerm-react 3.15.128 → 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.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/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
|
/>
|
|
@@ -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
|
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)
|