@electerm/electerm-react 3.9.15 → 3.11.0

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.
Files changed (33) hide show
  1. package/client/common/bookmark-schemas.js +164 -0
  2. package/client/common/default-setting.js +3 -2
  3. package/client/common/ws.js +25 -6
  4. package/client/common/zod.js +180 -0
  5. package/client/components/ai/agent-tool-call-card.jsx +90 -0
  6. package/client/components/ai/agent-tools.js +193 -0
  7. package/client/components/ai/agent.js +159 -0
  8. package/client/components/ai/ai-chat-entry.jsx +11 -0
  9. package/client/components/ai/ai-chat-history-item.jsx +48 -2
  10. package/client/components/ai/ai-chat.jsx +25 -6
  11. package/client/components/ai/ai-config.jsx +54 -5
  12. package/client/components/ai/ai.styl +73 -0
  13. package/client/components/batch-op/batch-op-runner.jsx +1 -2
  14. package/client/components/bookmark-form/common/bookmark-group-tree-format.js +1 -1
  15. package/client/components/bookmark-form/common/bookmark-select.jsx +1 -1
  16. package/client/components/bookmark-form/tree-select.jsx +1 -1
  17. package/client/components/main/main.jsx +3 -3
  18. package/client/components/rdp/file-transfer.js +3 -0
  19. package/client/components/setting-panel/setting-terminal.jsx +12 -0
  20. package/client/components/setting-panel/start-session-select.jsx +1 -1
  21. package/client/components/terminal/drop-file-modal.jsx +3 -3
  22. package/client/components/terminal/terminal-apis.js +8 -0
  23. package/client/components/terminal/terminal-error-handle.jsx +1 -1
  24. package/client/components/terminal/terminal-interactive-ui.jsx +157 -0
  25. package/client/components/terminal/terminal-interactive.jsx +65 -125
  26. package/client/components/terminal/terminal.jsx +28 -14
  27. package/client/components/terminal-info/base.jsx +25 -14
  28. package/client/components/terminal-info/log-path-edit.jsx +86 -0
  29. package/client/components/terminal-info/terminal-info-entry.jsx +11 -0
  30. package/client/components/text-editor/text-editor-entry.jsx +11 -0
  31. package/client/components/widgets/widget-form.jsx +30 -2
  32. package/client/entry/worker.js +9 -5
  33. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ import { agentTools, executeToolCall } from './agent-tools'
2
+
3
+ const MAX_ITERATIONS = 15
4
+
5
+ function buildAgentSystemPrompt (config) {
6
+ const lang = config.languageAI || window.store.getLangName()
7
+ const baseRole = config.roleAI || 'You are a helpful assistant.'
8
+ return `${baseRole}
9
+
10
+ You are operating inside electerm, a terminal/SSH client. You have access to tools that let you:
11
+ - Run commands in terminal tabs and read their output
12
+ - Open new terminal tabs (local or SSH)
13
+ - Manage bookmarks (create, list, open connections)
14
+ - Switch between tabs
15
+
16
+ When the user asks you to perform terminal operations, use the available tools.
17
+ Always explain what you are doing before executing commands.
18
+ If a command produces errors, analyze the output and try to fix the issue.
19
+ Prefer using the active terminal unless the user specifies otherwise.
20
+ For SSH connections, create a bookmark and open it rather than running ssh directly.
21
+
22
+ Reply in ${lang} language.`
23
+ }
24
+
25
+ function updateChatEntry (chatEntry, updates) {
26
+ const index = window.store.aiChatHistory.findIndex(i => i.id === chatEntry.id)
27
+ if (index !== -1) {
28
+ Object.assign(window.store.aiChatHistory[index], updates)
29
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
30
+ }
31
+ }
32
+
33
+ async function callBackendAIchatWithTools (messages, config) {
34
+ return window.pre.runGlobalAsync(
35
+ 'AIchatWithTools',
36
+ messages,
37
+ config.modelAI,
38
+ config.baseURLAI,
39
+ config.apiPathAI,
40
+ config.apiKeyAI,
41
+ config.proxyAI,
42
+ agentTools
43
+ )
44
+ }
45
+
46
+ export async function runAgentLoop (chatEntry, config, abortRef) {
47
+ const messages = [
48
+ { role: 'system', content: buildAgentSystemPrompt(config) },
49
+ { role: 'user', content: chatEntry.prompt }
50
+ ]
51
+ const toolCallsLog = []
52
+ let accumulatedContent = ''
53
+
54
+ updateChatEntry(chatEntry, {
55
+ isStreaming: true,
56
+ toolCalls: [],
57
+ response: ''
58
+ })
59
+
60
+ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
61
+ if (abortRef && abortRef.current) {
62
+ updateChatEntry(chatEntry, {
63
+ isStreaming: false,
64
+ response: accumulatedContent + '\n\n*(Agent stopped by user)*'
65
+ })
66
+ return
67
+ }
68
+
69
+ const result = await callBackendAIchatWithTools(messages, config)
70
+
71
+ if (result.error) {
72
+ updateChatEntry(chatEntry, {
73
+ isStreaming: false,
74
+ response: accumulatedContent + `\n\n**Error:** ${result.error}`
75
+ })
76
+ return
77
+ }
78
+
79
+ const assistantMessage = result.message
80
+ if (!assistantMessage) {
81
+ updateChatEntry(chatEntry, {
82
+ isStreaming: false,
83
+ response: accumulatedContent || 'No response from AI.'
84
+ })
85
+ return
86
+ }
87
+
88
+ messages.push(assistantMessage)
89
+
90
+ if (assistantMessage.content) {
91
+ accumulatedContent += (accumulatedContent ? '\n\n' : '') + assistantMessage.content
92
+ updateChatEntry(chatEntry, {
93
+ response: accumulatedContent
94
+ })
95
+ }
96
+
97
+ if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
98
+ updateChatEntry(chatEntry, {
99
+ isStreaming: false,
100
+ response: accumulatedContent
101
+ })
102
+ return
103
+ }
104
+
105
+ for (const toolCall of assistantMessage.tool_calls) {
106
+ if (abortRef && abortRef.current) {
107
+ updateChatEntry(chatEntry, {
108
+ isStreaming: false,
109
+ response: accumulatedContent + '\n\n*(Agent stopped by user)*'
110
+ })
111
+ return
112
+ }
113
+
114
+ let args
115
+ try {
116
+ args = JSON.parse(toolCall.function.arguments)
117
+ } catch {
118
+ args = {}
119
+ }
120
+
121
+ const toolEntry = {
122
+ id: toolCall.id,
123
+ name: toolCall.function.name,
124
+ args,
125
+ status: 'running',
126
+ result: null
127
+ }
128
+ toolCallsLog.push(toolEntry)
129
+ updateChatEntry(chatEntry, {
130
+ toolCalls: [...toolCallsLog]
131
+ })
132
+
133
+ let toolResult
134
+ try {
135
+ toolResult = await executeToolCall(toolCall.function.name, args)
136
+ toolEntry.status = 'completed'
137
+ toolEntry.result = toolResult
138
+ } catch (err) {
139
+ toolEntry.status = 'error'
140
+ toolEntry.result = err.message
141
+ }
142
+
143
+ updateChatEntry(chatEntry, {
144
+ toolCalls: [...toolCallsLog]
145
+ })
146
+
147
+ messages.push({
148
+ role: 'tool',
149
+ tool_call_id: toolCall.id,
150
+ content: toolEntry.result
151
+ })
152
+ }
153
+ }
154
+
155
+ updateChatEntry(chatEntry, {
156
+ isStreaming: false,
157
+ response: accumulatedContent + '\n\n*(Agent reached maximum iterations)*'
158
+ })
159
+ }
@@ -0,0 +1,11 @@
1
+ import { lazy, Suspense } from 'react'
2
+
3
+ const AIChat = lazy(() => import('./ai-chat'))
4
+
5
+ export default function AIChatEntry (props) {
6
+ return (
7
+ <Suspense fallback={null}>
8
+ <AIChat {...props} />
9
+ </Suspense>
10
+ )
11
+ }
@@ -1,6 +1,8 @@
1
1
  import { useState, useEffect, useRef, useCallback } from 'react'
2
2
  import AIOutput from './ai-output'
3
3
  import AIStopIcon from './ai-stop-icon'
4
+ import AgentToolCallCard from './agent-tool-call-card'
5
+ import { runAgentLoop } from './agent'
4
6
  import {
5
7
  Alert,
6
8
  Tooltip
@@ -17,6 +19,7 @@ import { copy } from '../../common/clipboard'
17
19
  export default function AIChatHistoryItem ({ item }) {
18
20
  const [showOutput, setShowOutput] = useState(true)
19
21
  const startedRef = useRef(false)
22
+ const abortRef = useRef(false)
20
23
  const {
21
24
  prompt,
22
25
  isStreaming,
@@ -28,7 +31,9 @@ export default function AIChatHistoryItem ({ item }) {
28
31
  apiPathAI,
29
32
  apiKeyAI,
30
33
  proxyAI,
31
- languageAI
34
+ languageAI,
35
+ mode,
36
+ toolCalls
32
37
  } = item
33
38
 
34
39
  function toggleOutput () {
@@ -108,15 +113,42 @@ export default function AIChatHistoryItem ({ item }) {
108
113
  }
109
114
  }, [prompt, modelAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, item.id, pollStreamContent])
110
115
 
116
+ const startAgentRequest = useCallback(async () => {
117
+ abortRef.current = false
118
+ const config = {
119
+ modelAI,
120
+ roleAI,
121
+ baseURLAI,
122
+ apiPathAI,
123
+ apiKeyAI,
124
+ proxyAI,
125
+ languageAI
126
+ }
127
+ await runAgentLoop(item, config, abortRef)
128
+ }, [modelAI, roleAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, languageAI, item.id])
129
+
111
130
  useEffect(() => {
112
131
  if (!response && !startedRef.current) {
113
132
  startedRef.current = true
114
- startRequest()
133
+ if (mode === 'agent') {
134
+ startAgentRequest()
135
+ } else {
136
+ startRequest()
137
+ }
115
138
  }
116
139
  }, [])
117
140
 
118
141
  async function handleStop (e) {
119
142
  e.stopPropagation()
143
+ if (mode === 'agent') {
144
+ abortRef.current = true
145
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
146
+ if (index !== -1) {
147
+ window.store.aiChatHistory[index].isStreaming = false
148
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
149
+ }
150
+ return
151
+ }
120
152
  if (!sessionId) return
121
153
 
122
154
  try {
@@ -194,6 +226,19 @@ export default function AIChatHistoryItem ({ item }) {
194
226
  )
195
227
  }
196
228
 
229
+ function renderToolCalls () {
230
+ if (mode !== 'agent' || !toolCalls || !toolCalls.length) {
231
+ return null
232
+ }
233
+ return (
234
+ <div className='agent-tool-calls'>
235
+ {toolCalls.map((tc) => (
236
+ <AgentToolCallCard key={tc.id} toolCall={tc} />
237
+ ))}
238
+ </div>
239
+ )
240
+ }
241
+
197
242
  return (
198
243
  <div className='chat-history-item'>
199
244
  <div className='mg1y'>
@@ -201,6 +246,7 @@ export default function AIChatHistoryItem ({ item }) {
201
246
  <Alert {...alertProps} />
202
247
  </Tooltip>
203
248
  </div>
249
+ {renderToolCalls()}
204
250
  {showOutput && <AIOutput item={item} />}
205
251
  </div>
206
252
  )
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useEffect } from 'react'
2
- import { Flex, Input } from 'antd'
2
+ import { Flex, Input, Segmented } from 'antd'
3
3
  import TabSelect from '../footer/tab-select'
4
4
  import AiChatHistory from './ai-chat-history'
5
5
  import uid from '../../common/uid'
@@ -21,6 +21,8 @@ const MAX_HISTORY = 100
21
21
 
22
22
  export default function AIChat (props) {
23
23
  const [prompt, setPrompt] = useState('')
24
+ const [mode, setMode] = useState('ask')
25
+ const isAgent = mode === 'agent'
24
26
 
25
27
  function handlePromptChange (e) {
26
28
  setPrompt(e.target.value)
@@ -38,6 +40,8 @@ export default function AIChat (props) {
38
40
  response: '',
39
41
  isStreaming: false,
40
42
  sessionId: null,
43
+ mode,
44
+ toolCalls: [],
41
45
  ...pick(props.config, [
42
46
  'modelAI',
43
47
  'roleAI',
@@ -57,7 +61,7 @@ export default function AIChat (props) {
57
61
  if (window.store.aiChatHistory.length > MAX_HISTORY) {
58
62
  window.store.aiChatHistory.splice(MAX_HISTORY)
59
63
  }
60
- }, [prompt])
64
+ }, [prompt, mode])
61
65
 
62
66
  function renderHistory () {
63
67
  return (
@@ -75,6 +79,19 @@ export default function AIChat (props) {
75
79
  window.store.aiChatHistory = []
76
80
  }
77
81
 
82
+ function renderTabSelect () {
83
+ if (isAgent) {
84
+ return null
85
+ }
86
+ return (
87
+ <TabSelect
88
+ selectedTabIds={props.selectedTabIds}
89
+ tabs={props.tabs}
90
+ activeTabId={props.activeTabId}
91
+ />
92
+ )
93
+ }
94
+
78
95
  function renderSendIcon () {
79
96
  return (
80
97
  <SendOutlined
@@ -126,11 +143,13 @@ export default function AIChat (props) {
126
143
  />
127
144
  <Flex className='ai-chat-terminals' justify='space-between' align='center'>
128
145
  <Flex align='center'>
129
- <TabSelect
130
- selectedTabIds={props.selectedTabIds}
131
- tabs={props.tabs}
132
- activeTabId={props.activeTabId}
146
+ <Segmented
147
+ options={['Ask', 'Agent']}
148
+ value={mode === 'ask' ? 'Ask' : 'Agent'}
149
+ onChange={(val) => setMode(val === 'Ask' ? 'ask' : 'agent')}
150
+ size='small'
133
151
  />
152
+ {renderTabSelect()}
134
153
  <SettingOutlined
135
154
  onClick={toggleConfig}
136
155
  className='mg1l pointer icon-hover toggle-ai-setting-icon'
@@ -6,7 +6,7 @@ import {
6
6
  Alert,
7
7
  Space
8
8
  } from 'antd'
9
- import { useEffect } from 'react'
9
+ import { useEffect, useState } from 'react'
10
10
  import Link from '../common/external-link'
11
11
  import AiCache from './ai-cache'
12
12
  import {
@@ -14,6 +14,7 @@ import {
14
14
  } from '../../common/constants'
15
15
  import Password from '../common/password'
16
16
  import AiHistory, { addHistoryItem } from './ai-history'
17
+ import message from '../common/message'
17
18
 
18
19
  const STORAGE_KEY_CONFIG = 'ai_config_history'
19
20
  const EVENT_NAME_CONFIG = 'ai-config-history-update'
@@ -36,6 +37,8 @@ const proxyOptions = [
36
37
 
37
38
  export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
38
39
  const [form] = Form.useForm()
40
+ const [testing, setTesting] = useState(false)
41
+ const baseURLAI = Form.useWatch('baseURLAI', form)
39
42
 
40
43
  useEffect(() => {
41
44
  if (initialValues) {
@@ -52,6 +55,37 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
52
55
  addHistoryItem(STORAGE_KEY_CONFIG, values, EVENT_NAME_CONFIG)
53
56
  }
54
57
 
58
+ const handleTest = async () => {
59
+ try {
60
+ const values = await form.validateFields()
61
+ setTesting(true)
62
+ const res = await window.pre.runGlobalAsync(
63
+ 'AIchat',
64
+ 'Hi',
65
+ values.modelAI,
66
+ values.roleAI,
67
+ values.baseURLAI,
68
+ values.apiPathAI,
69
+ values.apiKeyAI,
70
+ values.proxyAI,
71
+ false
72
+ )
73
+ if (res && res.error) {
74
+ message.error(res.error)
75
+ } else if (res && res.response) {
76
+ message.success('AI config works!')
77
+ } else {
78
+ message.error('Unexpected response from AI API')
79
+ }
80
+ } catch (e) {
81
+ if (e.message) {
82
+ message.error(e.message)
83
+ }
84
+ } finally {
85
+ setTesting(false)
86
+ }
87
+ }
88
+
55
89
  function handleSelectHistory (item) {
56
90
  if (item && typeof item === 'object') {
57
91
  form.setFieldsValue(item)
@@ -67,6 +101,13 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
67
101
  return { label, title }
68
102
  }
69
103
 
104
+ function renderApiUrlLabel () {
105
+ if (baseURLAI === 'https://api.atlascloud.ai/v1') {
106
+ return <span>API URL (<Link to='https://atlascloud.ai'>AtlasCloud</Link>)</span>
107
+ }
108
+ return 'API URL'
109
+ }
110
+
70
111
  if (!showAIConfig) {
71
112
  return null
72
113
  }
@@ -90,7 +131,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
90
131
  layout='vertical'
91
132
  className='ai-config-form'
92
133
  >
93
- <Form.Item label='API URL' required>
134
+ <Form.Item label={renderApiUrlLabel()} required>
94
135
  <Space.Compact className='width-100'>
95
136
  <Form.Item
96
137
  label='API URL'
@@ -178,9 +219,17 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
178
219
  </Form.Item>
179
220
 
180
221
  <Form.Item>
181
- <Button type='primary' htmlType='submit'>
182
- {e('save')}
183
- </Button>
222
+ <Space>
223
+ <Button type='primary' htmlType='submit'>
224
+ {e('save')}
225
+ </Button>
226
+ <Button
227
+ loading={testing}
228
+ onClick={handleTest}
229
+ >
230
+ {e('testConnection')}
231
+ </Button>
232
+ </Space>
184
233
  </Form.Item>
185
234
  </Form>
186
235
  <AiHistory
@@ -77,3 +77,76 @@
77
77
  align-items center
78
78
  .ai-stop-icon-square
79
79
  margin-left auto
80
+
81
+ .agent-tool-calls
82
+ margin 8px 0
83
+
84
+ .agent-tool-call-card
85
+ border 1px solid var(--main-darker)
86
+ border-radius 4px
87
+ margin-bottom 6px
88
+ font-size 13px
89
+
90
+ .agent-tool-header
91
+ display flex
92
+ align-items center
93
+ padding 6px 8px
94
+ gap 6px
95
+
96
+ .agent-tool-name
97
+ font-weight 500
98
+
99
+ .agent-tool-tag
100
+ margin-left auto
101
+
102
+ .agent-tool-status-running
103
+ color #1890ff
104
+ animation spin 1s linear infinite
105
+
106
+ .agent-tool-status-completed
107
+ color #52c41a
108
+
109
+ .agent-tool-status-error
110
+ color #ff4d4f
111
+
112
+ .agent-tool-detail
113
+ padding 0 8px 8px
114
+ border-top 1px solid var(--main-darker)
115
+
116
+ .agent-tool-args
117
+ margin-top 6px
118
+
119
+ .agent-tool-result
120
+ margin-top 6px
121
+
122
+ .agent-tool-label
123
+ font-weight 500
124
+ margin-bottom 4px
125
+ font-size 12px
126
+ color var(--text-color-2)
127
+
128
+ .agent-tool-pre
129
+ background var(--main-dark)
130
+ padding 6px 8px
131
+ border-radius 3px
132
+ font-size 12px
133
+ max-height 200px
134
+ overflow-y auto
135
+ white-space pre-wrap
136
+ word-break break-all
137
+ margin-bottom 0
138
+
139
+ .agent-tool-running
140
+ border-color #1890ff
141
+
142
+ .agent-tool-completed
143
+ border-color #52c41a
144
+
145
+ .agent-tool-error
146
+ border-color #ff4d4f
147
+
148
+ @keyframes spin
149
+ from
150
+ transform rotate(0deg)
151
+ to
152
+ transform rotate(360deg)
@@ -1,5 +1,5 @@
1
1
  import { Component } from 'react'
2
- import { refsStatic } from '../common/ref'
2
+ import { refsStatic, refs } from '../common/ref'
3
3
  import { statusMap } from '../../common/constants'
4
4
  import { autoRun } from 'manate'
5
5
  import uid from '../../common/uid'
@@ -237,7 +237,6 @@ export default class BatchOpRunner extends Component {
237
237
  throw new Error('No active tab. Please connect first.')
238
238
  }
239
239
 
240
- const { refs } = await import('../common/ref')
241
240
  const term = refs.get('term-' + tabId)
242
241
  if (!term || !term.term) {
243
242
  throw new Error('Terminal not found')
@@ -21,7 +21,7 @@ export default (bookmarkGroups = [], disabledId = '', returnMap = false, current
21
21
  }
22
22
  return y
23
23
  }
24
- const level1 = bookmarkGroups.filter(d => d.level !== 2)
24
+ const level1 = bookmarkGroups.filter(d => d.level === 1 || !d.level)
25
25
  .map(d => {
26
26
  const r = {
27
27
  title: d.title,
@@ -38,7 +38,7 @@ function buildTreeData (bookmarkGroups, tree) {
38
38
  if (!x) return ''
39
39
  return { value: x.id, key: x.id, title: createTitle(x) }
40
40
  }
41
- const level1 = cats.filter(d => d.level !== 2)
41
+ const level1 = cats.filter(d => d.level === 1 || !d.level)
42
42
  .map(d => {
43
43
  const r = {
44
44
  title: d.title,
@@ -79,7 +79,7 @@ function buildData (bookmarks, bookmarkGroups, searchText = '') {
79
79
  title: createTitleWithTag(x)
80
80
  }
81
81
  }
82
- const level1 = cats.filter(d => d.level !== 2)
82
+ const level1 = cats.filter(d => d.level === 1 || !d.level)
83
83
  .map(d => {
84
84
  const children = [
85
85
  ...(d.bookmarkGroupIds || []).map(buildSubCats),
@@ -4,7 +4,7 @@ import Layout from '../layout/layout'
4
4
  import FileInfoModal from '../sftp/file-info-modal'
5
5
  import UpdateCheck from './upgrade'
6
6
  import SettingModal from '../setting-panel/setting-modal'
7
- import TextEditor from '../text-editor/text-editor'
7
+ import TextEditor from '../text-editor/text-editor-entry'
8
8
  import Sidebar from '../sidebar'
9
9
  import CssOverwrite from '../bg/css-overwrite'
10
10
  import UiTheme from './ui-theme'
@@ -19,7 +19,6 @@ import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
19
19
  import classnames from 'classnames'
20
20
  import ShortcutControl from '../shortcuts/shortcut-control.jsx'
21
21
  import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
22
- import TerminalInfo from '../terminal-info/terminal-info'
23
22
  import { ConfigProvider } from 'antd'
24
23
  import { NotificationContainer } from '../common/notification'
25
24
  import InfoModal from '../sidebar/info-modal.jsx'
@@ -27,7 +26,7 @@ import RightSidePanel from '../side-panel-r/side-panel-r'
27
26
  import ConnectionHoppingWarning from './connection-hopping-warnning'
28
27
  import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
29
28
  import LoadSshConfigs from '../ssh-config/load-ssh-configs'
30
- import AIChat from '../ai/ai-chat'
29
+ import AIChat from '../ai/ai-chat-entry'
31
30
  import AIConfigModal from '../ai/ai-config-modal'
32
31
  import Opacity from '../common/opacity'
33
32
  import MoveItemModal from '../tree-list/move-item-modal'
@@ -40,6 +39,7 @@ import UnixTimestampTooltip from '../terminal/unix-timestamp-tooltip'
40
39
  import { pick } from 'lodash-es'
41
40
  import deepCopy from 'json-deep-copy'
42
41
  import './wrapper.styl'
42
+ import TerminalInfo from '../terminal-info/terminal-info-entry'
43
43
  import './term-fullscreen.styl'
44
44
 
45
45
  export default auto(function Index (props) {
@@ -273,6 +273,9 @@ export class FileTransferManager {
273
273
  })
274
274
 
275
275
  this.log(`Downloaded ${fileInfo.name} (${filesize(fileInfo._totalSize)}) to ${fullPath}`, 'success')
276
+ this.hasRemoteFiles = false
277
+ this.pendingDownloads.clear()
278
+ this.notifyStateChange()
276
279
  if (this.onDownloadComplete) {
277
280
  this.onDownloadComplete(fullPath, fileInfo.name, fileInfo._totalSize)
278
281
  }
@@ -84,6 +84,7 @@ export default class SettingTerminal extends Component {
84
84
 
85
85
  handleChangeDelMode = v => this.onChangeValue(v, 'backspaceMode')
86
86
  handleChangeRenderType = v => this.onChangeValue(v, 'rendererType')
87
+ handleChangeDragDropBehavior = v => this.onChangeValue(v, 'dragDropBehavior')
87
88
 
88
89
  handleChangeFont = (values) => {
89
90
  this.onChangeValue(
@@ -471,6 +472,7 @@ export default class SettingTerminal extends Component {
471
472
  const {
472
473
  rendererType,
473
474
  backspaceMode = '^?',
475
+ dragDropBehavior = 'ask',
474
476
  keywords = [{ color: 'red' }]
475
477
  } = this.props.config
476
478
  const {
@@ -593,6 +595,16 @@ export default class SettingTerminal extends Component {
593
595
  'autoReconnectTerminal'
594
596
  ].map(d => this.renderToggle(d))
595
597
  }
598
+ <div className='pd1b'>{e('dragDropBehavior')}</div>
599
+ <Select
600
+ onChange={this.handleChangeDragDropBehavior}
601
+ value={dragDropBehavior}
602
+ popupMatchSelectWidth={false}
603
+ >
604
+ {['ask', 'trz', 'rz', 'inputOnly'].map(id => (
605
+ <Option key={id} value={id}>{e(id)}</Option>
606
+ ))}
607
+ </Select>
596
608
  <div className='pd1b'>{e('terminalBackSpaceMode')}</div>
597
609
  <Select
598
610
  onChange={this.handleChangeDelMode}
@@ -65,7 +65,7 @@ function BookmarkSelect (props) {
65
65
  title: createTitleWithTag(x)
66
66
  }
67
67
  }
68
- const level1 = cats.filter(d => d.level !== 2)
68
+ const level1 = cats.filter(d => d.level === 1 || !d.level)
69
69
  .map(d => {
70
70
  const r = {
71
71
  title: d.title,
@@ -26,21 +26,21 @@ export class DropFileModal extends Component {
26
26
  <button
27
27
  type='button'
28
28
  className='custom-modal-ok-btn'
29
- onClick={() => onSelect('trzUpload')}
29
+ onClick={() => onSelect('trz')}
30
30
  >
31
31
  trz
32
32
  </button>
33
33
  <button
34
34
  type='button'
35
35
  className='custom-modal-cancel-btn'
36
- onClick={() => onSelect('rzUpload')}
36
+ onClick={() => onSelect('rz')}
37
37
  >
38
38
  rz
39
39
  </button>
40
40
  <button
41
41
  type='button'
42
42
  className='custom-modal-cancel-btn'
43
- onClick={() => onSelect('inputPath')}
43
+ onClick={() => onSelect('inputOnly')}
44
44
  >
45
45
  {e('inputOnly')}
46
46
  </button>