@electerm/electerm-react 3.1.26 → 3.3.8

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 (61) hide show
  1. package/client/common/constants.js +1 -3
  2. package/client/common/db.js +4 -2
  3. package/client/components/ai/ai-history.jsx +4 -4
  4. package/client/components/batch-op/batch-op-alert.jsx +42 -0
  5. package/client/components/batch-op/batch-op-editor.jsx +202 -0
  6. package/client/components/batch-op/batch-op-logs.jsx +53 -0
  7. package/client/components/batch-op/batch-op-runner.jsx +315 -0
  8. package/client/components/bookmark-form/ai-bookmark-form.jsx +2 -1
  9. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +2 -1
  10. package/client/components/bookmark-form/common/bookmark-select.jsx +18 -2
  11. package/client/components/bookmark-form/common/connection-hopping-form.jsx +153 -0
  12. package/client/components/bookmark-form/common/connection-hopping.jsx +136 -129
  13. package/client/components/common/auto-check-update.jsx +31 -0
  14. package/client/components/common/notification.styl +1 -1
  15. package/client/components/file-transfer/conflict-resolve.jsx +3 -0
  16. package/client/components/footer/batch-input.jsx +10 -7
  17. package/client/components/main/error-wrapper.jsx +18 -7
  18. package/client/components/main/main.jsx +6 -7
  19. package/client/components/quick-commands/qm.styl +0 -2
  20. package/client/components/quick-commands/quick-commands-list-form.jsx +1 -1
  21. package/client/components/setting-panel/hotkey.jsx +9 -1
  22. package/client/components/setting-panel/list.jsx +0 -1
  23. package/client/components/setting-panel/list.styl +4 -0
  24. package/client/components/setting-panel/setting-modal.jsx +53 -47
  25. package/client/components/setting-sync/auto-sync.jsx +53 -0
  26. package/client/components/setting-sync/data-import.jsx +69 -8
  27. package/client/components/sftp/address-bar.jsx +7 -1
  28. package/client/components/shortcuts/shortcut-editor.jsx +4 -2
  29. package/client/components/sidebar/bookmark-select.jsx +3 -2
  30. package/client/components/sidebar/history-item.jsx +3 -1
  31. package/client/components/sidebar/history.jsx +1 -0
  32. package/client/components/sidebar/index.jsx +0 -9
  33. package/client/components/tabs/add-btn-menu.jsx +1 -1
  34. package/client/components/tabs/add-btn.jsx +9 -15
  35. package/client/components/tabs/quick-connect.jsx +6 -10
  36. package/client/components/terminal/attach-addon-custom.js +86 -0
  37. package/client/components/terminal/cmd-item.jsx +13 -3
  38. package/client/components/terminal/drop-file-modal.jsx +57 -0
  39. package/client/components/terminal/terminal-command-dropdown.jsx +91 -13
  40. package/client/components/terminal/terminal.jsx +107 -10
  41. package/client/components/terminal/terminal.styl +9 -0
  42. package/client/components/tree-list/tree-list-item.jsx +0 -1
  43. package/client/components/tree-list/tree-list.jsx +115 -10
  44. package/client/components/tree-list/tree-list.styl +3 -0
  45. package/client/components/tree-list/tree-search.jsx +9 -1
  46. package/client/components/vnc/vnc-session.jsx +2 -0
  47. package/client/components/widgets/widget-control.jsx +3 -0
  48. package/client/components/widgets/widget-form.jsx +6 -0
  49. package/client/components/widgets/widget-instance.jsx +26 -7
  50. package/client/css/includes/box.styl +3 -0
  51. package/client/store/common.js +0 -28
  52. package/client/store/init-state.js +2 -1
  53. package/client/store/load-data.js +6 -4
  54. package/client/store/mcp-handler.js +20 -2
  55. package/client/store/sync.js +25 -1
  56. package/client/store/tab.js +1 -1
  57. package/client/store/watch.js +10 -18
  58. package/client/store/widgets.js +54 -0
  59. package/client/views/index.pug +1 -2
  60. package/package.json +1 -1
  61. package/client/components/batch-op/batch-op.jsx +0 -694
@@ -242,15 +242,13 @@ export const quickCommandLabelsLsKey = 'quick-command-label'
242
242
  export const localAddrBookmarkLsKey = 'local-addr-bookmark-keys'
243
243
  export const dismissDelKeyTipLsKey = 'dismiss-del-key-tip'
244
244
  export const sshTunnelHelpLink = 'https://github.com/electerm/electerm/wiki/How-to-use-ssh-tunnel'
245
- export const batchOpHelpLink = 'https://github.com/electerm/electerm/wiki/batch-operation'
246
245
  export const proxyHelpLink = 'https://github.com/electerm/electerm/wiki/proxy-format'
247
246
  export const regexHelpLink = 'https://github.com/electerm/electerm/wiki/Terminal-keywords-highlight-regular-expression-exmaples'
248
247
  export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/wiki/Connection-Hopping-Behavior-Change-in-electerm-since-v1.50.65'
249
248
  export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
250
249
  export const modals = {
251
250
  hide: 0,
252
- setting: 1,
253
- batchOps: 2
251
+ setting: 1
254
252
  }
255
253
  export const instSftpKeys = [
256
254
  'connect',
@@ -31,7 +31,8 @@ export const dbNames = [
31
31
  ),
32
32
  'history',
33
33
  'terminalCommandHistory',
34
- 'aiChatHistory'
34
+ 'aiChatHistory',
35
+ 'autoRunWidgets'
35
36
  ]
36
37
  export const dbNamesForSync = [
37
38
  ...without(
@@ -45,7 +46,8 @@ export const dbNamesForWatch = [
45
46
  ...dbNamesForSync,
46
47
  'history',
47
48
  'terminalCommandHistory',
48
- 'aiChatHistory'
49
+ 'aiChatHistory',
50
+ 'autoRunWidgets'
49
51
  ]
50
52
 
51
53
  /**
@@ -4,14 +4,14 @@
4
4
  import { useState, useEffect } from 'react'
5
5
  import { Space } from 'antd'
6
6
  import { HistoryOutlined } from '@ant-design/icons'
7
- import { getItemJSON, setItemJSON } from '../../common/safe-local-storage'
7
+ import { safeGetItemJSON, safeSetItemJSON } from '../../common/safe-local-storage'
8
8
  import AiHistoryItem from './ai-history-item'
9
9
 
10
10
  const MAX_HISTORY = 20
11
11
  const e = window.translate
12
12
 
13
13
  export function getHistory (storageKey) {
14
- return getItemJSON(storageKey, [])
14
+ return safeGetItemJSON(storageKey, [])
15
15
  }
16
16
 
17
17
  export function addHistoryItem (storageKey, itemData, eventName) {
@@ -37,7 +37,7 @@ export function addHistoryItem (storageKey, itemData, eventName) {
37
37
  if (history.length > MAX_HISTORY) {
38
38
  history = history.slice(0, MAX_HISTORY)
39
39
  }
40
- setItemJSON(storageKey, history)
40
+ safeSetItemJSON(storageKey, history)
41
41
 
42
42
  // Custom event to trigger update
43
43
  if (eventName) {
@@ -72,7 +72,7 @@ export default function AiHistory (props) {
72
72
  return hStr !== itemStr
73
73
  })
74
74
  setHistory(newHistory)
75
- setItemJSON(storageKey, newHistory)
75
+ safeSetItemJSON(storageKey, newHistory)
76
76
  }
77
77
 
78
78
  if (!history.length) {
@@ -0,0 +1,42 @@
1
+ import React, { useState } from 'react'
2
+ import { Alert, Button } from 'antd'
3
+ import ExternalLink from '../common/external-link'
4
+
5
+ const batchOpWikiLink = 'https://github.com/electerm/electerm/wiki/batch-operation'
6
+
7
+ export default function BatchOpAlert () {
8
+ const [expanded, setExpanded] = useState(false)
9
+
10
+ const description = (
11
+ <div>
12
+ <div>
13
+ <p>Actions: <code>connect, command, sftp_upload, sftp_download</code></p>
14
+ <p><ExternalLink to={batchOpWikiLink}>{batchOpWikiLink}</ExternalLink></p>
15
+ </div>
16
+ {expanded && (
17
+ <div className='mg1t'>
18
+ <p><strong>connect</strong> params: <code>host, port, username, authType, password, privateKey, passphrase, certificate, profile, enableSftp, enableSsh, useSshAgent, sshAgent, term, encode, envLang, setEnv, startDirectoryRemote, startDirectoryLocal, proxy, x11, displayRaw, sshTunnels, connectionHoppings</code></p>
19
+ <p><strong>command</strong> params: <code>command</code></p>
20
+ <p><strong>sftp_upload</strong> params: <code>localPath, remotePath</code></p>
21
+ <p><strong>sftp_download</strong> params: <code>remotePath, localPath</code></p>
22
+ </div>
23
+ )}
24
+ <Button
25
+ size='small'
26
+ className='mg1y'
27
+ onClick={() => setExpanded(v => !v)}
28
+ >
29
+ {expanded ? 'Show less' : 'Show more'}
30
+ </Button>
31
+ </div>
32
+ )
33
+
34
+ return (
35
+ <Alert
36
+ description={description}
37
+ type='info'
38
+ showIcon
39
+ className='mg1b'
40
+ />
41
+ )
42
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Batch Operation Editor Component
3
+ * Self-contained workflow editor: handles execute, external editors, and progress logs
4
+ */
5
+ import React, { useCallback, useState, useEffect } from 'react'
6
+ import { Button, Flex } from 'antd'
7
+ import {
8
+ PlayCircleOutlined
9
+ } from '@ant-design/icons'
10
+ import SimpleEditor from '../text-editor/simple-editor'
11
+ import EditWithCustomEditor from '../text-editor/edit-with-custom-editor'
12
+ import BatchOpAlert from './batch-op-alert'
13
+ import BatchOpLogs from './batch-op-logs'
14
+ import message from '../common/message'
15
+ import { refsStatic } from '../common/ref'
16
+ import generate from '../../common/uid'
17
+ import fs from '../../common/fs'
18
+
19
+ const workflowExample = `[
20
+ {
21
+ "name": "Connect SSH",
22
+ "action": "connect",
23
+ "params": {
24
+ "host": "192.168.1.100",
25
+ "port": 22,
26
+ "username": "root",
27
+ "authType": "password",
28
+ "password": "your_password"
29
+ }
30
+ },
31
+ {
32
+ "name": "Create 5M Test File",
33
+ "action": "command",
34
+ "afterDelay": 500,
35
+ "prevDelay": 500,
36
+ "command": "fallocate -l 5M /tmp/test_5m_file.bin && rm -f /tmp/test_log.log && echo '[LOG] Created 5M test file at $(date)' >> /tmp/test_log.log"
37
+ },
38
+ {
39
+ "name": "Log creation",
40
+ "action": "command",
41
+ "command": "ls -la /tmp/test_5m_file.bin >> /tmp/test_log.log 2>&1 && echo '[LOG] File size logged at $(date)' >> /tmp/test_log.log"
42
+ },
43
+ {
44
+ "name": "Download 5M File",
45
+ "action": "sftp_download",
46
+ "afterDelay": 200,
47
+ "remotePath": "/tmp/test_5m_file.bin",
48
+ "localPath": "/tmp/test_5m_file.bin"
49
+ },
50
+ {
51
+ "name": "Log after download",
52
+ "action": "command",
53
+ "afterDelay": 200,
54
+ "command": "echo '[LOG] Download complete at $(date)' >> /tmp/test_log.log"
55
+ },
56
+ {
57
+ "name": "Delete Remote 5M File",
58
+ "action": "command",
59
+ "afterDelay": 200,
60
+ "command": "rm /tmp/test_5m_file.bin && echo '[LOG] Deleted remote 5M file at $(date)' >> /tmp/test_log.log"
61
+ },
62
+ {
63
+ "name": "Upload Downloaded File to Remote",
64
+ "action": "sftp_upload",
65
+ "afterDelay": 200,
66
+ "localPath": "/tmp/test_5m_file.bin",
67
+ "remotePath": "/tmp/test_5m_file_uploaded.bin"
68
+ },
69
+ {
70
+ "name": "Log after upload",
71
+ "action": "command",
72
+ "afterDelay": 200,
73
+ "command": "echo '[LOG] Upload complete at $(date)' >> /tmp/test_log.log"
74
+ },
75
+ {
76
+ "name": "Verify and clean up",
77
+ "action": "command",
78
+ "command": "ls -la /tmp/test_5m_file_uploaded.bin >> /tmp/test_log.log 2>&1 && rm -f /tmp/test_5m_file*.bin && echo '[LOG] Cleaned up at $(date)' >> /tmp/test_log.log"
79
+ }
80
+ ]`
81
+
82
+ function getDefaultValue (widget) {
83
+ if (widget?.info?.configs) {
84
+ const config = widget.info.configs.find(c => c.name === 'workflowJson')
85
+ if (config?.default) return config.default
86
+ }
87
+ return ''
88
+ }
89
+
90
+ export default function BatchOpEditor ({ widget }) {
91
+ const [value, setValue] = useState(() => getDefaultValue(widget))
92
+ const [executing, setExecuting] = useState(false)
93
+
94
+ useEffect(() => {
95
+ const v = getDefaultValue(widget)
96
+ if (v) setValue(v)
97
+ }, [widget?.id])
98
+
99
+ const handleExecute = async () => {
100
+ if (!value || executing) return
101
+ setExecuting(true)
102
+ const runner = refsStatic.get('batch-op-runner')
103
+ runner?.reset()
104
+ refsStatic.get('batch-op-logs')?.setLogs({ steps: [], currentIndex: 0, status: 'running' })
105
+ try {
106
+ let workflows
107
+ try {
108
+ workflows = JSON.parse(value)
109
+ if (!Array.isArray(workflows)) throw new Error('Workflow must be an array')
110
+ } catch (e) {
111
+ message.error('Invalid workflow JSON: ' + e.message)
112
+ refsStatic.get('batch-op-logs')?.reset()
113
+ return
114
+ }
115
+ await runner.executeWorkflow(workflows)
116
+ message.success('Workflow execution completed')
117
+ } catch (err) {
118
+ if (err.message !== 'Workflow aborted') {
119
+ message.error('Workflow execution failed: ' + err.message)
120
+ }
121
+ } finally {
122
+ setExecuting(false)
123
+ }
124
+ }
125
+
126
+ const handleTemplate = useCallback(() => {
127
+ setValue(workflowExample)
128
+ }, [])
129
+
130
+ const handleEditWithSystemEditor = useCallback(async () => {
131
+ const id = generate()
132
+ const tempPath = window.pre.resolve(window.pre.tempDir, `electerm-batch-op-${id}.json`)
133
+ await fs.writeFile(tempPath, value)
134
+ window.pre.runGlobalAsync('watchFile', tempPath)
135
+ fs.openFile(tempPath).catch(window.store.onError)
136
+ window.pre.showItemInFolder(tempPath)
137
+ const onFileChange = (e, text) => {
138
+ setValue(text)
139
+ window.pre.ipcOffEvent('file-change', onFileChange)
140
+ fs.unlink(tempPath).catch(console.log)
141
+ }
142
+ window.pre.ipcOnEvent('file-change', onFileChange)
143
+ }, [value])
144
+
145
+ const handleEditWithCustom = useCallback(async (editorCommand) => {
146
+ const id = generate()
147
+ const tempPath = window.pre.resolve(window.pre.tempDir, `electerm-batch-op-${id}.json`)
148
+ await fs.writeFile(tempPath, value)
149
+ window.pre.runGlobalAsync('watchFile', tempPath)
150
+ await window.pre.runGlobalAsync('openFileWithEditor', tempPath, editorCommand)
151
+ const onFileChange = (e, text) => {
152
+ setValue(text)
153
+ window.pre.ipcOffEvent('file-change', onFileChange)
154
+ fs.unlink(tempPath).catch(console.log)
155
+ }
156
+ window.pre.ipcOnEvent('file-change', onFileChange)
157
+ }, [value])
158
+
159
+ function handleChange (e) {
160
+ setValue(e.target.value)
161
+ }
162
+
163
+ return (
164
+ <div className='batch-op-editor'>
165
+ <BatchOpAlert />
166
+ <Flex className='mg2y' gap='small'>
167
+ <Button onClick={handleTemplate} type='dashed'>
168
+ Load Template
169
+ </Button>
170
+ <Button
171
+ onClick={handleExecute}
172
+ type='primary'
173
+ loading={executing}
174
+ disabled={executing}
175
+ icon={<PlayCircleOutlined />}
176
+ >
177
+ Execute Workflow
178
+ </Button>
179
+ </Flex>
180
+ <SimpleEditor
181
+ value={value}
182
+ onChange={handleChange}
183
+ />
184
+ {!window.et.isWebApp && (
185
+ <div className='pd1t pd2b'>
186
+ <Button
187
+ type='primary'
188
+ className='mg1r mg1b'
189
+ onClick={handleEditWithSystemEditor}
190
+ >
191
+ {window.translate('editWithSystemEditor')}
192
+ </Button>
193
+ <EditWithCustomEditor
194
+ loading={executing}
195
+ editWithCustom={handleEditWithCustom}
196
+ />
197
+ </div>
198
+ )}
199
+ <BatchOpLogs />
200
+ </div>
201
+ )
202
+ }
@@ -0,0 +1,53 @@
1
+ import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react'
2
+ import { refsStatic } from '../common/ref'
3
+
4
+ const STATIC_KEY = 'batch-op-logs'
5
+
6
+ const BatchOpLogs = forwardRef(function BatchOpLogs (_, ref) {
7
+ const [logs, setLogsState] = useState(null)
8
+
9
+ useImperativeHandle(ref, () => ({
10
+ setLogs: (progress) => setLogsState(progress),
11
+ reset: () => setLogsState(null)
12
+ }))
13
+
14
+ useEffect(() => {
15
+ refsStatic.add(STATIC_KEY, {
16
+ setLogs: (progress) => setLogsState(progress),
17
+ reset: () => setLogsState(null)
18
+ })
19
+ return () => refsStatic.remove(STATIC_KEY)
20
+ }, [])
21
+
22
+ if (!logs || (!logs.steps.length && !logs.currentStep)) {
23
+ return null
24
+ }
25
+
26
+ const statusIcon = { success: '✓', error: '✗' }
27
+
28
+ return (
29
+ <div className='batch-op-logs mg1t pd1 font13'>
30
+ <div className='bold mg1b'>Execution Log</div>
31
+ {logs.steps.map((step, i) => (
32
+ <div key={i} className={`batch-op-log-entry ${step.status}`}>
33
+ <span className='log-icon mg1r'>{statusIcon[step.status] || '○'}</span>
34
+ <span className='log-name'>{step.name}</span>
35
+ {step.error && <span className='log-error mg1l color-red'>{step.error}</span>}
36
+ </div>
37
+ ))}
38
+ {logs.status === 'running' && logs.currentStep && (
39
+ <div className='batch-op-log-entry running'>
40
+ <span className='log-icon mg1r'>→</span>
41
+ <span className='log-name'>{logs.currentStep}</span>
42
+ </div>
43
+ )}
44
+ {logs.status === 'completed' && (
45
+ <div className='batch-op-log-entry completed color-green mg1t'>
46
+ ✓ Workflow completed
47
+ </div>
48
+ )}
49
+ </div>
50
+ )
51
+ })
52
+
53
+ export default BatchOpLogs
@@ -0,0 +1,315 @@
1
+ import { Component } from 'react'
2
+ import { refsStatic } from '../common/ref'
3
+ import { statusMap } from '../../common/constants'
4
+ import { autoRun } from 'manate'
5
+ import uid from '../../common/uid'
6
+
7
+ const STATIC_KEY = 'batch-op-runner'
8
+
9
+ export default class BatchOpRunner extends Component {
10
+ constructor () {
11
+ super()
12
+ this.steps = []
13
+ this.currentIndex = 0
14
+ this.status = 'idle'
15
+ this.currentTabId = null
16
+ this.currentStep = null
17
+ }
18
+
19
+ componentDidMount () {
20
+ refsStatic.add(STATIC_KEY, this)
21
+ }
22
+
23
+ getActiveTabId () {
24
+ return window.store?.activeTabId
25
+ }
26
+
27
+ getState () {
28
+ return {
29
+ steps: this.steps,
30
+ currentIndex: this.currentIndex,
31
+ status: this.status,
32
+ currentStep: this.currentStep
33
+ }
34
+ }
35
+
36
+ reset () {
37
+ this.steps = []
38
+ this.currentIndex = 0
39
+ this.status = 'idle'
40
+ this.currentTabId = null
41
+ this.currentStep = null
42
+ }
43
+
44
+ runBatchOpFromFile (filePath) {
45
+ return this._runBatchOpFromFile(filePath)
46
+ }
47
+
48
+ executeWorkflow (workflows) {
49
+ return this._executeWorkflow(workflows)
50
+ }
51
+
52
+ async _runBatchOpFromFile (filePath) {
53
+ try {
54
+ const content = await window.fs.readFile(filePath)
55
+ let workflows
56
+ try {
57
+ workflows = JSON.parse(content)
58
+ if (!Array.isArray(workflows)) {
59
+ throw new Error('Workflow must be an array')
60
+ }
61
+ } catch (e) {
62
+ console.error('Invalid batch operation JSON:', e.message)
63
+ return
64
+ }
65
+
66
+ this.reset()
67
+ await this._executeWorkflow(workflows)
68
+ console.log('Batch operation completed from file')
69
+ } catch (e) {
70
+ console.error('Failed to run batch operation from file:', e.message)
71
+ }
72
+ }
73
+
74
+ async _executeWorkflow (workflows) {
75
+ if (!Array.isArray(workflows)) {
76
+ throw new Error('Workflow must be an array')
77
+ }
78
+
79
+ this.steps = []
80
+ this.currentIndex = 0
81
+ this.status = 'running'
82
+
83
+ const results = []
84
+ const logsRef = refsStatic.get('batch-op-logs')
85
+
86
+ for (let i = 0; i < workflows.length; i++) {
87
+ const step = workflows[i]
88
+ this.currentIndex = i
89
+ this.currentStep = step.name
90
+
91
+ logsRef?.setLogs(this.getState())
92
+
93
+ let result
94
+ try {
95
+ result = await this._executeStep(step, results)
96
+ this.steps.push({ name: step.name, status: 'success', result })
97
+ results.push(result)
98
+ console.log(`Batch op step ${i + 1} completed:`, step.name || 'unnamed')
99
+ } catch (e) {
100
+ console.log(e)
101
+ this.steps.push({ name: step.name, status: 'error', error: e.message })
102
+ logsRef?.setLogs(this.getState())
103
+ console.error(`Batch op step ${i + 1} failed:`, step.name || 'unnamed', e.message)
104
+ this.status = 'error'
105
+ logsRef?.setLogs(this.getState())
106
+ throw e
107
+ }
108
+ }
109
+
110
+ this.status = 'completed'
111
+ this.currentStep = null
112
+ logsRef?.setLogs(this.getState())
113
+ }
114
+
115
+ async _executeStep (step, previousResults) {
116
+ const { action, prevDelay, afterDelay } = step
117
+
118
+ if (!action) {
119
+ throw new Error('Step must have an "action" field')
120
+ }
121
+
122
+ if (prevDelay > 0) {
123
+ await new Promise(resolve => setTimeout(resolve, prevDelay))
124
+ }
125
+
126
+ const s = step.params ? { ...step, ...step.params } : step
127
+
128
+ let result
129
+ switch (action) {
130
+ case 'connect':
131
+ result = await this._batchStepConnect(s)
132
+ this.currentTabId = result.tabId
133
+ break
134
+ case 'command':
135
+ result = await this._batchStepCommand(s)
136
+ break
137
+ case 'sftp_upload':
138
+ result = await this._batchStepSftpUpload(s)
139
+ break
140
+ case 'sftp_download':
141
+ result = await this._batchStepSftpDownload(s)
142
+ break
143
+ default:
144
+ throw new Error(`Unknown action: ${action}`)
145
+ }
146
+
147
+ if (afterDelay > 0) {
148
+ await new Promise(resolve => setTimeout(resolve, afterDelay))
149
+ }
150
+
151
+ return result
152
+ }
153
+
154
+ async _batchStepConnect (step) {
155
+ const { store } = window
156
+ const p = step.params || step
157
+
158
+ const tabId = uid()
159
+ const tab = {
160
+ id: tabId,
161
+ type: 'ssh',
162
+ host: p.host || '',
163
+ port: p.port || 22,
164
+ username: p.username || '',
165
+ password: p.password || '',
166
+ privateKey: p.privateKey || '',
167
+ passphrase: p.passphrase || '',
168
+ certificate: p.certificate || '',
169
+ authType: p.authType || 'password',
170
+ profile: p.profile || '',
171
+ enableSftp: p.enableSftp !== false,
172
+ enableSsh: p.enableSsh !== false,
173
+ useSshAgent: p.useSshAgent !== false,
174
+ sshAgent: p.sshAgent || '',
175
+ term: p.term || 'xterm-256color',
176
+ encode: p.encode || 'utf8',
177
+ envLang: p.envLang || 'en_US.UTF-8',
178
+ setEnv: p.setEnv || '',
179
+ startDirectoryRemote: p.startDirectoryRemote || '',
180
+ startDirectoryLocal: p.startDirectoryLocal || '',
181
+ proxy: p.proxy || '',
182
+ x11: p.x11 || false,
183
+ displayRaw: p.displayRaw || false,
184
+ sshTunnels: p.sshTunnels || [],
185
+ connectionHoppings: p.connectionHoppings || [],
186
+ title: step.name || `SSH: ${p.host}`,
187
+ status: 'processing',
188
+ pane: 'terminal'
189
+ }
190
+
191
+ store.addTab(tab)
192
+ await this._waitForConnection(tabId)
193
+
194
+ return {
195
+ success: true,
196
+ action: 'connect',
197
+ host: p.host,
198
+ port: p.port,
199
+ tabId
200
+ }
201
+ }
202
+
203
+ _waitForConnection = async (tabId) => {
204
+ return new Promise((resolve, reject) => {
205
+ const timeout = setTimeout(() => {
206
+ if (this._refWait) {
207
+ this._refWait.stop()
208
+ delete this._refWait
209
+ }
210
+ reject(new Error('Connection timeout'))
211
+ }, 30000)
212
+
213
+ this._refWait = autoRun(() => {
214
+ const { tabs } = window.store
215
+ const tab = tabs.find(t => t.id === tabId)
216
+ if (tab && tab.status === statusMap.success) {
217
+ clearTimeout(timeout)
218
+ this._refWait && this._refWait.stop()
219
+ delete this._refWait
220
+ resolve(tab)
221
+ } else if (tab && tab.status === statusMap.error) {
222
+ clearTimeout(timeout)
223
+ this._refWait && this._refWait.stop()
224
+ delete this._refWait
225
+ reject(new Error('Connection failed: ' + (tab.errorMsg || 'unknown error')))
226
+ }
227
+ return window.store.tabs
228
+ })
229
+ this._refWait.start()
230
+ })
231
+ }
232
+
233
+ async _batchStepCommand (step) {
234
+ const tabId = this.currentTabId
235
+
236
+ if (!tabId) {
237
+ throw new Error('No active tab. Please connect first.')
238
+ }
239
+
240
+ const { refs } = await import('../common/ref')
241
+ const term = refs.get('term-' + tabId)
242
+ if (!term || !term.term) {
243
+ throw new Error('Terminal not found')
244
+ }
245
+
246
+ let waited = 0
247
+ while (!term.attachAddon && waited < 10000) {
248
+ await new Promise(resolve => setTimeout(resolve, 200))
249
+ waited += 200
250
+ }
251
+ if (!term.attachAddon) {
252
+ throw new Error('Terminal not ready: attach addon not initialized')
253
+ }
254
+
255
+ term.runQuickCommand(step.command)
256
+
257
+ return {
258
+ success: true,
259
+ action: 'command',
260
+ command: step.command,
261
+ tabId
262
+ }
263
+ }
264
+
265
+ async _batchStepSftpUpload (step) {
266
+ const tabId = this.currentTabId
267
+ const { store } = window
268
+ const stepWithTabId = { ...step, tabId, conflictPolicy: 'mergeOrOverwriteAll' }
269
+ const { transferId } = await store.mcpSftpUpload(stepWithTabId)
270
+ await this._batchWaitForTransfer(transferId)
271
+ return { success: true, action: 'sftp_upload', localPath: step.localPath, remotePath: step.remotePath, transferId, tabId }
272
+ }
273
+
274
+ async _batchStepSftpDownload (step) {
275
+ const tabId = this.currentTabId
276
+ const { store } = window
277
+ const stepWithTabId = { ...step, tabId, conflictPolicy: 'mergeOrOverwriteAll' }
278
+ const { transferId } = await store.mcpSftpDownload(stepWithTabId)
279
+ await this._batchWaitForTransfer(transferId)
280
+ return { success: true, action: 'sftp_download', remotePath: step.remotePath, localPath: step.localPath, transferId, tabId }
281
+ }
282
+
283
+ async _batchWaitForTransfer (transferId) {
284
+ return new Promise((resolve, reject) => {
285
+ const timeout = setTimeout(() => {
286
+ if (this._refTransferWait) {
287
+ this._refTransferWait.stop()
288
+ delete this._refTransferWait
289
+ }
290
+ reject(new Error('Transfer timeout (1 hour)'))
291
+ }, 60 * 60 * 1000)
292
+
293
+ this._refTransferWait = autoRun(() => {
294
+ const { transferHistory } = window.store
295
+ const item = transferHistory.find(t => t.id === transferId || t.originalId === transferId)
296
+ if (item) {
297
+ clearTimeout(timeout)
298
+ this._refTransferWait && this._refTransferWait.stop()
299
+ delete this._refTransferWait
300
+ if (item.error) {
301
+ reject(new Error('Transfer failed: ' + item.error))
302
+ } else {
303
+ resolve(item)
304
+ }
305
+ }
306
+ return window.store.transferHistory
307
+ })
308
+ this._refTransferWait.start()
309
+ })
310
+ }
311
+
312
+ render () {
313
+ return null
314
+ }
315
+ }
@@ -2,7 +2,8 @@
2
2
  * AI-powered bookmark generation form
3
3
  */
4
4
  import { useState, useEffect } from 'react'
5
- import { Button, Input, message, Space, Alert } from 'antd'
5
+ import { Button, Input, Space, Alert } from 'antd'
6
+ import message from '../common/message'
6
7
  import {
7
8
  RobotOutlined,
8
9
  LoadingOutlined,