@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.
@@ -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.-]+@[\w.-]+/.test(input)) {
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
- const messages = [
53
- { role: 'system', content: buildAgentSystemPrompt(config) },
54
- { role: 'user', content: chatEntry.prompt }
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, Popconfirm, Segmented } from 'antd'
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
- UnorderedListOutlined
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 = 100
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={props.aiChatHistory}
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 clearHistory () {
90
- window.store.aiChatHistory = []
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
- .clear-ai-icon
71
- position relative
72
- &::after
73
- content '×'
74
- position absolute
75
- font-size 12px
76
- top 8px
77
- right -4px
78
- font-weight bold
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 = (id) => {
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, shortcut } = this.props.item
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
- <Button
45
- key={id}
46
- {...btnProps}
110
+ <Tooltip
111
+ title={this.renderTooltip()}
112
+ placement='top'
113
+ mouseEnterDelay={0.5}
114
+ overlayClassName='qm-tooltip-overlay'
47
115
  >
48
- {name}
49
- </Button>
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 || obj.name.toLowerCase().includes(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
@@ -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 if (
118
- options.initFolder &&
119
- !(store.config.onStartSessions || []).length &&
120
- store.config.initDefaultTabOnStart
121
- ) {
122
- window.initFolder = options.initFolder
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.15.128",
3
+ "version": "3.15.138",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",