@electerm/electerm-react 2.3.166 → 2.3.176

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.
@@ -32,7 +32,7 @@ export default async function download (filename, text) {
32
32
  return
33
33
  }
34
34
  notification.success({
35
- message: '',
35
+ title: '',
36
36
  description: (
37
37
  <ShowItem
38
38
  to={filePath}
@@ -20,7 +20,7 @@ export default (e) => {
20
20
  </div>
21
21
  )
22
22
  notification.error({
23
- message: msg,
23
+ title: msg,
24
24
  description,
25
25
  duration: 55
26
26
  })
@@ -30,7 +30,7 @@ export async function handleErr (res) {
30
30
  }
31
31
  log.debug(text, 'fetch err info')
32
32
  notification.error({
33
- message: 'error',
33
+ title: 'error',
34
34
  description: (
35
35
  <div className='common-err'>
36
36
  {text}
@@ -27,7 +27,7 @@ export default function ConnectionHoppingWarning (props) {
27
27
  return
28
28
  }
29
29
  notification.info({
30
- message: e('connectionHopping'),
30
+ title: e('connectionHopping'),
31
31
  duration: 0,
32
32
  placement: 'bottom',
33
33
  key: connectionHoppingWarnKey,
@@ -17,16 +17,6 @@
17
17
  .qm-list-wrap
18
18
  max-height 100px
19
19
 
20
- .fil-keyword
21
- .fil-label
22
- .qm-item
23
- color var(--text-disabled)
24
- .qm-item
25
- &.name-match
26
- &.label-match
27
- font-weight bold
28
- color var(--text)
29
-
30
20
  .qm-search-input
31
21
  max-width 200px
32
22
  @media (max-width: 500px)
@@ -18,17 +18,14 @@ export default class QuickCommandsItem extends PureComponent {
18
18
  }
19
19
 
20
20
  render () {
21
- const { name, id, nameMatch, labelMatch, shortcut } = this.props.item
21
+ const { name, id, shortcut } = this.props.item
22
22
  const {
23
23
  draggable,
24
24
  handleDragOver,
25
25
  handleDragStart,
26
26
  handleDrop
27
27
  } = this.props
28
- const cls = classNames('qm-item mg1r mg1b', {
29
- 'name-match': nameMatch,
30
- 'label-match': labelMatch
31
- })
28
+ const cls = classNames('qm-item mg1r mg1b')
32
29
  const btnProps = {
33
30
  className: cls,
34
31
  onClick: this.handleSelect,
@@ -129,15 +129,12 @@ export default function QuickCommandsFooterBox (props) {
129
129
  )
130
130
  }
131
131
 
132
- function sortArray (array, keyword, labels, qmSortByFrequency) {
133
- const sorters = [
134
- (obj) => !(keyword && obj.name.toLowerCase().includes(keyword)),
135
- (obj) => !labels.some((label) => (obj.labels || []).includes(label))
136
- ]
137
- if (qmSortByFrequency) {
138
- sorters.push((obj) => -(obj.clickCount || 0))
139
- }
140
- return sortBy(array, sorters)
132
+ function filterArray (array, keyword, labels) {
133
+ return array.filter(obj => {
134
+ const nameMatches = !keyword || obj.name.toLowerCase().includes(keyword)
135
+ const labelMatches = !labels.length || labels.some((label) => (obj.labels || []).includes(label))
136
+ return nameMatches && labelMatches
137
+ })
141
138
  }
142
139
 
143
140
  const {
@@ -156,14 +153,10 @@ export default function QuickCommandsFooterBox (props) {
156
153
  return renderNoCmd()
157
154
  }
158
155
  const keyword0 = keyword.toLowerCase()
159
- const filtered = sortArray(all, keyword0, labels, qmSortByFrequency)
160
- .map(d => {
161
- return {
162
- ...d,
163
- nameMatch: keyword && d.name.toLowerCase().includes(keyword),
164
- labelMatch: labels.some((label) => (d.labels || []).includes(label))
165
- }
166
- })
156
+ const filtered = filterArray(all, keyword0, labels)
157
+ const sorted = qmSortByFrequency
158
+ ? sortBy(filtered, (obj) => -(obj.clickCount || 0))
159
+ : filtered
167
160
  const sprops = {
168
161
  value: labels,
169
162
  mode: 'multiple',
@@ -174,11 +167,7 @@ export default function QuickCommandsFooterBox (props) {
174
167
  const tp = pinnedQuickCommandBar
175
168
  ? 'primary'
176
169
  : 'text'
177
- const cls = classNames(
178
- 'qm-list-wrap',
179
- { 'fil-label': !!labels.length },
180
- { 'fil-keyword': !!keyword }
181
- )
170
+ const cls = classNames('qm-list-wrap')
182
171
  const type = qmSortByFrequency ? 'primary' : 'default'
183
172
  const w = openedSideBar ? 43 + leftSidebarWidth : 43
184
173
  const qmProps = {
@@ -233,7 +222,7 @@ export default function QuickCommandsFooterBox (props) {
233
222
  </Space.Compact>
234
223
  </Flex>
235
224
  <div className={cls}>
236
- {filtered.map(renderItem)}
225
+ {sorted.map(renderItem)}
237
226
  </div>
238
227
  </div>
239
228
  </div>
@@ -39,7 +39,7 @@ export default auto(function SettingModalWrap (props) {
39
39
  shouldConfirmDel: tabsShouldConfirmDel.includes(settingTab),
40
40
  list: settingSidebarList
41
41
  }
42
- const { bookmarks, bookmarkGroups } = store
42
+ const { bookmarks, bookmarkGroups, widgetInstances } = store
43
43
  const formProps = {
44
44
  store,
45
45
  formData: settingItem,
@@ -51,6 +51,7 @@ export default auto(function SettingModalWrap (props) {
51
51
  ]),
52
52
  bookmarkGroups,
53
53
  bookmarks,
54
+ widgetInstancesLength: widgetInstances.length,
54
55
  serials: store.serials,
55
56
  loaddingSerials: store.loaddingSerials
56
57
  }
@@ -62,7 +62,7 @@ export default function SyncForm (props) {
62
62
  const test = await window.store.testSyncToken(syncType, res.gistId)
63
63
  if (!test) {
64
64
  return notification.error({
65
- message: 'token invalid'
65
+ title: 'token invalid'
66
66
  })
67
67
  }
68
68
  if (!res.gistId && syncType !== syncTypes.custom && syncType !== syncTypes.cloud) {
@@ -981,7 +981,7 @@ export default class Sftp extends Component {
981
981
  const np = this.parsePath(type, this.state[nt])
982
982
  if (!isValidPath(np)) {
983
983
  return notification.warning({
984
- message: 'path not valid'
984
+ title: 'path not valid'
985
985
  })
986
986
  }
987
987
  this.setState({
@@ -66,7 +66,7 @@
66
66
  .sftp-item
67
67
  position absolute
68
68
  left 0
69
- right 0
69
+ right 10px
70
70
  bottom 0
71
71
  top 0
72
72
  z-index 1
@@ -20,7 +20,7 @@ function handleIgnore () {
20
20
 
21
21
  function showNotification () {
22
22
  notification.info({
23
- message: e('loadSshConfigs'),
23
+ title: e('loadSshConfigs'),
24
24
  duration: 0,
25
25
  placement: 'bottom',
26
26
  key: 'sshConfigNotify',
@@ -1320,7 +1320,7 @@ class Term extends Component {
1320
1320
  }
1321
1321
  this.socketCloseWarning = notification.warning({
1322
1322
  key,
1323
- message: e('socketCloseTip'),
1323
+ title: e('socketCloseTip'),
1324
1324
  duration: 30,
1325
1325
  description: (
1326
1326
  <div className='pd2y'>
@@ -5,7 +5,7 @@ import React, { useState } from 'react'
5
5
  import WidgetForm from './widget-form'
6
6
  import { showMsg } from './widget-notification-with-details'
7
7
 
8
- export default function WidgetControl ({ formData }) {
8
+ export default function WidgetControl ({ formData, widgetInstancesLength }) {
9
9
  const [loading, setLoading] = useState(false)
10
10
  const widget = formData
11
11
  if (!widget.id) {
@@ -16,6 +16,12 @@ export default function WidgetControl ({ formData }) {
16
16
  )
17
17
  }
18
18
 
19
+ // Check if this widget already has a running instance
20
+ // widgetInstancesLength is used to trigger re-render when instances change
21
+ const hasRunningInstance = widgetInstancesLength > 0 && window.store.widgetInstances.some(
22
+ instance => instance.widgetId === widget.id
23
+ )
24
+
19
25
  const handleFormSubmit = async (config) => {
20
26
  setLoading(true)
21
27
  try {
@@ -57,6 +63,7 @@ export default function WidgetControl ({ formData }) {
57
63
  widget={widget}
58
64
  onSubmit={handleFormSubmit}
59
65
  loading={loading}
66
+ hasRunningInstance={hasRunningInstance}
60
67
  />
61
68
  </div>
62
69
  )
@@ -2,10 +2,10 @@
2
2
  * Widget form component
3
3
  */
4
4
  import React from 'react'
5
- import { Form, Input, InputNumber, Switch, Select, Button } from 'antd'
5
+ import { Form, Input, InputNumber, Switch, Select, Button, Tooltip } from 'antd'
6
6
  import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
7
7
 
8
- export default function WidgetForm ({ widget, onSubmit, loading }) {
8
+ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInstance }) {
9
9
  const [form] = Form.useForm()
10
10
 
11
11
  if (!widget) {
@@ -13,9 +13,10 @@ export default function WidgetForm ({ widget, onSubmit, loading }) {
13
13
  }
14
14
 
15
15
  const { info } = widget
16
- const { configs, type } = info
16
+ const { configs, type, singleInstance } = info
17
17
  const isInstanceWidget = type === 'instance'
18
18
  const txt = isInstanceWidget ? 'Start widget' : 'Run widget'
19
+ const isDisabled = loading || (singleInstance && hasRunningInstance)
19
20
 
20
21
  const handleSubmit = async (values) => {
21
22
  onSubmit(values)
@@ -95,14 +96,16 @@ export default function WidgetForm ({ widget, onSubmit, loading }) {
95
96
  <Form.Item
96
97
  {...tailFormItemLayout}
97
98
  >
98
- <Button
99
- type='primary'
100
- htmlType='submit'
101
- loading={loading}
102
- disabled={loading}
103
- >
104
- {txt}
105
- </Button>
99
+ <Tooltip title={isDisabled && singleInstance && hasRunningInstance ? 'Already running, only one instance allowed' : ''}>
100
+ <Button
101
+ type='primary'
102
+ htmlType='submit'
103
+ loading={loading}
104
+ disabled={isDisabled}
105
+ >
106
+ {txt}
107
+ </Button>
108
+ </Tooltip>
106
109
  </Form.Item>
107
110
  </Form>
108
111
  </div>
@@ -95,10 +95,10 @@ for $i, $index in 5 16 32
95
95
  overflow hidden
96
96
 
97
97
  .overscroll
98
- overflow scroll
98
+ overflow auto
99
99
 
100
100
  .overscroll-y
101
- overflow-y scroll
101
+ overflow-y auto
102
102
 
103
103
  .relative
104
104
  position relative
@@ -4,7 +4,7 @@
4
4
 
5
5
  import handleError from '../common/error-handler'
6
6
  import { Modal } from 'antd'
7
- import { debounce, some, get } from 'lodash-es'
7
+ import { debounce, some, get, pickBy } from 'lodash-es'
8
8
  import {
9
9
  modals,
10
10
  leftSidebarWidthKey,
@@ -219,27 +219,31 @@ export default Store => {
219
219
  delete p.name
220
220
  delete p.id
221
221
  if (type === connectionMap.rdp) {
222
+ const filtered = pickBy(p.rdp, (value) => value !== undefined && value !== '')
222
223
  return {
223
224
  ...tab,
224
- ...p.rdp
225
+ ...filtered
225
226
  }
226
227
  } else if (type === connectionMap.vnc) {
228
+ const filtered = pickBy(p.vnc, (value) => value !== undefined && value !== '')
227
229
  return {
228
230
  ...tab,
229
- ...p.vnc
231
+ ...filtered
230
232
  }
231
233
  } else if (type === connectionMap.telnet) {
234
+ const filtered = pickBy(p.telnet, (value) => value !== undefined && value !== '')
232
235
  return {
233
236
  ...tab,
234
- ...p.telnet
237
+ ...filtered
235
238
  }
236
239
  }
237
240
  delete p.rdp
238
241
  delete p.vnc
239
242
  delete p.telnet
243
+ const filtered = pickBy(p, (value) => value !== undefined && value !== '')
240
244
  return {
241
245
  ...tab,
242
- ...p
246
+ ...filtered
243
247
  }
244
248
  }
245
249
  Store.prototype.applyProfileToTabs = function (tab) {
@@ -0,0 +1,640 @@
1
+ /**
2
+ * MCP (Model Context Protocol) handler for store
3
+ * Handles IPC requests from the MCP server widget
4
+ */
5
+
6
+ import uid from '../common/uid'
7
+ import { settingMap } from '../common/constants'
8
+
9
+ export default Store => {
10
+ // Initialize MCP handler - called when MCP widget is started
11
+ Store.prototype.initMcpHandler = function () {
12
+ const { ipcOnEvent } = window.pre
13
+ // Listen for MCP requests from main process
14
+ ipcOnEvent('mcp-request', (event, request) => {
15
+ const { requestId, action, data } = request
16
+ if (action === 'tool-call') {
17
+ window.store.handleMcpToolCall(requestId, data.toolName, data.args)
18
+ }
19
+ })
20
+ }
21
+
22
+ // Handle individual tool calls
23
+ Store.prototype.handleMcpToolCall = async function (requestId, toolName, args) {
24
+ const { store } = window
25
+
26
+ try {
27
+ let result
28
+
29
+ switch (toolName) {
30
+ // Bookmark operations
31
+ case 'list_bookmarks':
32
+ result = store.mcpListBookmarks(args)
33
+ break
34
+ case 'get_bookmark':
35
+ result = store.mcpGetBookmark(args)
36
+ break
37
+ case 'add_bookmark':
38
+ result = await store.mcpAddBookmark(args)
39
+ break
40
+ case 'edit_bookmark':
41
+ result = store.mcpEditBookmark(args)
42
+ break
43
+ case 'delete_bookmark':
44
+ result = store.mcpDeleteBookmark(args)
45
+ break
46
+ case 'open_bookmark':
47
+ result = store.mcpOpenBookmark(args)
48
+ break
49
+
50
+ // Bookmark group operations
51
+ case 'list_bookmark_groups':
52
+ result = store.mcpListBookmarkGroups()
53
+ break
54
+ case 'add_bookmark_group':
55
+ result = await store.mcpAddBookmarkGroup(args)
56
+ break
57
+
58
+ // Quick command operations
59
+ case 'list_quick_commands':
60
+ result = store.mcpListQuickCommands()
61
+ break
62
+ case 'add_quick_command':
63
+ result = store.mcpAddQuickCommand(args)
64
+ break
65
+ case 'run_quick_command':
66
+ result = store.mcpRunQuickCommand(args)
67
+ break
68
+ case 'delete_quick_command':
69
+ result = store.mcpDeleteQuickCommand(args)
70
+ break
71
+
72
+ // Tab operations
73
+ case 'list_tabs':
74
+ result = store.mcpListTabs()
75
+ break
76
+ case 'get_active_tab':
77
+ result = store.mcpGetActiveTab()
78
+ break
79
+ case 'switch_tab':
80
+ result = store.mcpSwitchTab(args)
81
+ break
82
+ case 'close_tab':
83
+ result = store.mcpCloseTab(args)
84
+ break
85
+ case 'reload_tab':
86
+ result = store.mcpReloadTab(args)
87
+ break
88
+ case 'duplicate_tab':
89
+ result = store.mcpDuplicateTab(args)
90
+ break
91
+ case 'open_local_terminal':
92
+ result = store.mcpOpenLocalTerminal()
93
+ break
94
+
95
+ // Terminal operations
96
+ case 'send_terminal_command':
97
+ result = store.mcpSendTerminalCommand(args)
98
+ break
99
+ case 'get_terminal_selection':
100
+ result = store.mcpGetTerminalSelection(args)
101
+ break
102
+ case 'get_terminal_output':
103
+ result = store.mcpGetTerminalOutput(args)
104
+ break
105
+
106
+ // History operations
107
+ case 'list_history':
108
+ result = store.mcpListHistory(args)
109
+ break
110
+ case 'clear_history':
111
+ result = store.mcpClearHistory()
112
+ break
113
+
114
+ // Transfer operations
115
+ case 'list_transfers':
116
+ result = store.mcpListTransfers()
117
+ break
118
+ case 'list_transfer_history':
119
+ result = store.mcpListTransferHistory(args)
120
+ break
121
+
122
+ // Settings operations
123
+ case 'get_settings':
124
+ result = store.mcpGetSettings()
125
+ break
126
+ case 'list_terminal_themes':
127
+ result = store.mcpListTerminalThemes()
128
+ break
129
+ case 'list_ui_themes':
130
+ result = store.mcpListUiThemes()
131
+ break
132
+
133
+ default:
134
+ throw new Error(`Unknown tool: ${toolName}`)
135
+ }
136
+
137
+ window.api.sendMcpResponse({
138
+ requestId,
139
+ result
140
+ })
141
+ } catch (error) {
142
+ window.api.sendMcpResponse({
143
+ requestId,
144
+ error: error.message
145
+ })
146
+ }
147
+ }
148
+
149
+ // ==================== Bookmark APIs ====================
150
+
151
+ Store.prototype.mcpListBookmarks = function (args = {}) {
152
+ const { store } = window
153
+ let bookmarks = store.bookmarks
154
+
155
+ if (args.groupId) {
156
+ const group = store.bookmarkGroups.find(g => g.id === args.groupId)
157
+ if (group && group.bookmarkIds) {
158
+ const idSet = new Set(group.bookmarkIds)
159
+ bookmarks = bookmarks.filter(b => idSet.has(b.id))
160
+ }
161
+ }
162
+
163
+ return bookmarks.map(b => ({
164
+ id: b.id,
165
+ title: b.title,
166
+ host: b.host,
167
+ port: b.port,
168
+ username: b.username,
169
+ type: b.type || 'ssh',
170
+ color: b.color
171
+ }))
172
+ }
173
+
174
+ Store.prototype.mcpGetBookmark = function (args) {
175
+ const { store } = window
176
+ const bookmark = store.bookmarks.find(b => b.id === args.id)
177
+ if (!bookmark) {
178
+ throw new Error(`Bookmark not found: ${args.id}`)
179
+ }
180
+ // Return bookmark without sensitive data
181
+ const { password, passphrase, privateKey, ...safeBookmark } = bookmark
182
+ return safeBookmark
183
+ }
184
+
185
+ Store.prototype.mcpAddBookmark = async function (args) {
186
+ const { store } = window
187
+ const bookmark = {
188
+ id: uid(),
189
+ title: args.title,
190
+ host: args.host || '',
191
+ port: args.port || 22,
192
+ username: args.username || '',
193
+ password: args.password || '',
194
+ type: args.type || 'local',
195
+ term: 'xterm-256color',
196
+ ...args
197
+ }
198
+
199
+ store.addItem(bookmark, settingMap.bookmarks)
200
+
201
+ return {
202
+ success: true,
203
+ id: bookmark.id,
204
+ message: `Bookmark "${bookmark.title}" created`
205
+ }
206
+ }
207
+
208
+ Store.prototype.mcpEditBookmark = function (args) {
209
+ const { store } = window
210
+ const { id, updates } = args
211
+
212
+ const bookmark = store.bookmarks.find(b => b.id === id)
213
+ if (!bookmark) {
214
+ throw new Error(`Bookmark not found: ${id}`)
215
+ }
216
+
217
+ store.editItem(id, updates, settingMap.bookmarks)
218
+
219
+ return {
220
+ success: true,
221
+ message: `Bookmark "${bookmark.title}" updated`
222
+ }
223
+ }
224
+
225
+ Store.prototype.mcpDeleteBookmark = function (args) {
226
+ const { store } = window
227
+ const bookmark = store.bookmarks.find(b => b.id === args.id)
228
+ if (!bookmark) {
229
+ throw new Error(`Bookmark not found: ${args.id}`)
230
+ }
231
+
232
+ store.delItem({ id: args.id }, settingMap.bookmarks)
233
+
234
+ return {
235
+ success: true,
236
+ message: `Bookmark "${bookmark.title}" deleted`
237
+ }
238
+ }
239
+
240
+ Store.prototype.mcpOpenBookmark = function (args) {
241
+ const { store } = window
242
+ const bookmark = store.bookmarks.find(b => b.id === args.id)
243
+ if (!bookmark) {
244
+ throw new Error(`Bookmark not found: ${args.id}`)
245
+ }
246
+
247
+ store.onSelectBookmark(args.id)
248
+
249
+ return {
250
+ success: true,
251
+ message: `Opened bookmark "${bookmark.title}"`
252
+ }
253
+ }
254
+
255
+ // ==================== Bookmark Group APIs ====================
256
+
257
+ Store.prototype.mcpListBookmarkGroups = function () {
258
+ const { store } = window
259
+ return store.bookmarkGroups.map(g => ({
260
+ id: g.id,
261
+ title: g.title,
262
+ level: g.level,
263
+ bookmarkCount: (g.bookmarkIds || []).length,
264
+ subgroupCount: (g.bookmarkGroupIds || []).length
265
+ }))
266
+ }
267
+
268
+ Store.prototype.mcpAddBookmarkGroup = async function (args) {
269
+ const { store } = window
270
+ const group = {
271
+ id: uid(),
272
+ title: args.title,
273
+ bookmarkIds: [],
274
+ bookmarkGroupIds: [],
275
+ level: args.parentId ? 2 : 1
276
+ }
277
+
278
+ await store.addBookmarkGroup(group)
279
+
280
+ return {
281
+ success: true,
282
+ id: group.id,
283
+ message: `Bookmark group "${group.title}" created`
284
+ }
285
+ }
286
+
287
+ // ==================== Quick Command APIs ====================
288
+
289
+ Store.prototype.mcpListQuickCommands = function () {
290
+ const { store } = window
291
+ return store.quickCommands.map(q => ({
292
+ id: q.id,
293
+ name: q.name,
294
+ command: q.command,
295
+ commands: q.commands,
296
+ inputOnly: q.inputOnly,
297
+ labels: q.labels
298
+ }))
299
+ }
300
+
301
+ Store.prototype.mcpAddQuickCommand = function (args) {
302
+ const { store } = window
303
+ const qm = {
304
+ id: uid(),
305
+ name: args.name,
306
+ command: args.command,
307
+ inputOnly: args.inputOnly || false,
308
+ labels: args.labels || []
309
+ }
310
+
311
+ store.addQuickCommand(qm)
312
+
313
+ return {
314
+ success: true,
315
+ id: qm.id,
316
+ message: `Quick command "${qm.name}" created`
317
+ }
318
+ }
319
+
320
+ Store.prototype.mcpRunQuickCommand = function (args) {
321
+ const { store } = window
322
+ const qm = store.quickCommands.find(q => q.id === args.id)
323
+ if (!qm) {
324
+ throw new Error(`Quick command not found: ${args.id}`)
325
+ }
326
+
327
+ store.runQuickCommandItem(args.id)
328
+
329
+ return {
330
+ success: true,
331
+ message: `Executed quick command "${qm.name}"`
332
+ }
333
+ }
334
+
335
+ Store.prototype.mcpDeleteQuickCommand = function (args) {
336
+ const { store } = window
337
+ const qm = store.quickCommands.find(q => q.id === args.id)
338
+ if (!qm) {
339
+ throw new Error(`Quick command not found: ${args.id}`)
340
+ }
341
+
342
+ store.delQuickCommand({ id: args.id })
343
+
344
+ return {
345
+ success: true,
346
+ message: `Deleted quick command "${qm.name}"`
347
+ }
348
+ }
349
+
350
+ // ==================== Tab APIs ====================
351
+
352
+ Store.prototype.mcpListTabs = function () {
353
+ const { store } = window
354
+ return store.tabs.map(t => ({
355
+ id: t.id,
356
+ title: t.title,
357
+ host: t.host,
358
+ type: t.type || 'local',
359
+ status: t.status,
360
+ isTransporting: t.isTransporting,
361
+ batch: t.batch
362
+ }))
363
+ }
364
+
365
+ Store.prototype.mcpGetActiveTab = function () {
366
+ const { store } = window
367
+ const tab = store.currentTab
368
+ if (!tab) {
369
+ return { activeTabId: null, tab: null }
370
+ }
371
+ return {
372
+ activeTabId: store.activeTabId,
373
+ tab: {
374
+ id: tab.id,
375
+ title: tab.title,
376
+ host: tab.host,
377
+ type: tab.type || 'local',
378
+ status: tab.status
379
+ }
380
+ }
381
+ }
382
+
383
+ Store.prototype.mcpSwitchTab = function (args) {
384
+ const { store } = window
385
+ const tab = store.tabs.find(t => t.id === args.tabId)
386
+ if (!tab) {
387
+ throw new Error(`Tab not found: ${args.tabId}`)
388
+ }
389
+
390
+ store.activeTabId = args.tabId
391
+ if (tab.batch !== undefined) {
392
+ store[`activeTabId${tab.batch}`] = args.tabId
393
+ }
394
+
395
+ return {
396
+ success: true,
397
+ message: `Switched to tab "${tab.title}"`
398
+ }
399
+ }
400
+
401
+ Store.prototype.mcpCloseTab = function (args) {
402
+ const { store } = window
403
+ const tab = store.tabs.find(t => t.id === args.tabId)
404
+ if (!tab) {
405
+ throw new Error(`Tab not found: ${args.tabId}`)
406
+ }
407
+
408
+ store.delTab(args.tabId)
409
+
410
+ return {
411
+ success: true,
412
+ message: `Closed tab "${tab.title}"`
413
+ }
414
+ }
415
+
416
+ Store.prototype.mcpReloadTab = function (args) {
417
+ const { store } = window
418
+ const tabId = args.tabId || store.activeTabId
419
+ const tab = store.tabs.find(t => t.id === tabId)
420
+ if (!tab) {
421
+ throw new Error(`Tab not found: ${tabId}`)
422
+ }
423
+
424
+ store.reloadTab(tabId)
425
+
426
+ return {
427
+ success: true,
428
+ message: `Reloaded tab "${tab.title}"`
429
+ }
430
+ }
431
+
432
+ Store.prototype.mcpDuplicateTab = function (args) {
433
+ const { store } = window
434
+ const tab = store.tabs.find(t => t.id === args.tabId)
435
+ if (!tab) {
436
+ throw new Error(`Tab not found: ${args.tabId}`)
437
+ }
438
+
439
+ store.duplicateTab(args.tabId)
440
+
441
+ return {
442
+ success: true,
443
+ message: `Duplicated tab "${tab.title}"`
444
+ }
445
+ }
446
+
447
+ Store.prototype.mcpOpenLocalTerminal = function () {
448
+ const { store } = window
449
+ store.addTab()
450
+ const newTabId = store.activeTabId
451
+
452
+ return {
453
+ success: true,
454
+ tabId: newTabId,
455
+ message: 'Opened new local terminal'
456
+ }
457
+ }
458
+
459
+ // ==================== Terminal APIs ====================
460
+
461
+ Store.prototype.mcpSendTerminalCommand = function (args) {
462
+ const { store } = window
463
+ const tabId = args.tabId || store.activeTabId
464
+ const command = args.command
465
+
466
+ if (!tabId) {
467
+ throw new Error('No active terminal')
468
+ }
469
+
470
+ if (command === undefined || command === null) {
471
+ throw new Error('No command provided')
472
+ }
473
+
474
+ store.runQuickCommand(command, args.inputOnly || false)
475
+
476
+ return {
477
+ success: true,
478
+ message: 'Command sent to terminal'
479
+ }
480
+ }
481
+
482
+ Store.prototype.mcpGetTerminalSelection = function (args) {
483
+ const { store } = window
484
+ const { refs } = require('../components/common/ref')
485
+ const tabId = args.tabId || store.activeTabId
486
+
487
+ if (!tabId) {
488
+ throw new Error('No active terminal')
489
+ }
490
+
491
+ const term = refs.get('term-' + tabId)
492
+ if (!term || !term.term) {
493
+ throw new Error('Terminal not found')
494
+ }
495
+
496
+ const selection = term.term.getSelection()
497
+
498
+ return {
499
+ selection: selection || '',
500
+ tabId
501
+ }
502
+ }
503
+
504
+ Store.prototype.mcpGetTerminalOutput = function (args) {
505
+ const { store } = window
506
+ const { refs } = require('../components/common/ref')
507
+ const tabId = args.tabId || store.activeTabId
508
+ const lineCount = args.lines || 50
509
+
510
+ if (!tabId) {
511
+ throw new Error('No active terminal')
512
+ }
513
+
514
+ const term = refs.get('term-' + tabId)
515
+ if (!term || !term.term) {
516
+ throw new Error('Terminal not found')
517
+ }
518
+
519
+ const buffer = term.term.buffer.active
520
+ if (!buffer) {
521
+ throw new Error('Terminal buffer not available')
522
+ }
523
+
524
+ const cursorY = buffer.cursorY || 0
525
+ const baseY = buffer.baseY || 0
526
+ const totalLines = buffer.length || 0
527
+
528
+ // Calculate the actual content range
529
+ // baseY is the scroll offset, cursorY is cursor position in viewport
530
+ const actualContentEnd = baseY + cursorY + 1
531
+ const startLine = Math.max(0, actualContentEnd - lineCount)
532
+ const endLine = Math.min(totalLines, actualContentEnd)
533
+ const lines = []
534
+
535
+ for (let i = startLine; i < endLine; i++) {
536
+ const line = buffer.getLine(i)
537
+ if (line) {
538
+ const text = line.translateToString(true)
539
+ lines.push(text)
540
+ }
541
+ }
542
+
543
+ return {
544
+ output: lines.join('\n'),
545
+ lineCount: lines.length,
546
+ cursorY,
547
+ baseY,
548
+ tabId
549
+ }
550
+ }
551
+
552
+ // ==================== History APIs ====================
553
+
554
+ Store.prototype.mcpListHistory = function (args = {}) {
555
+ const { store } = window
556
+ const limit = args.limit || 50
557
+ const history = store.history.slice(0, limit)
558
+
559
+ return history.map(h => ({
560
+ id: h.id,
561
+ title: h.title,
562
+ host: h.host,
563
+ type: h.type,
564
+ time: h.time
565
+ }))
566
+ }
567
+
568
+ Store.prototype.mcpClearHistory = function () {
569
+ const { store } = window
570
+ store.history = []
571
+
572
+ return {
573
+ success: true,
574
+ message: 'History cleared'
575
+ }
576
+ }
577
+
578
+ // ==================== Transfer APIs ====================
579
+
580
+ Store.prototype.mcpListTransfers = function () {
581
+ const { store } = window
582
+ return store.fileTransfers.map(t => ({
583
+ id: t.id,
584
+ localPath: t.localPath,
585
+ remotePath: t.remotePath,
586
+ type: t.type,
587
+ percent: t.percent,
588
+ status: t.status
589
+ }))
590
+ }
591
+
592
+ Store.prototype.mcpListTransferHistory = function (args = {}) {
593
+ const { store } = window
594
+ const limit = args.limit || 50
595
+ return store.transferHistory.slice(0, limit).map(t => ({
596
+ id: t.id,
597
+ localPath: t.localPath,
598
+ remotePath: t.remotePath,
599
+ type: t.type,
600
+ status: t.status,
601
+ time: t.time
602
+ }))
603
+ }
604
+
605
+ // ==================== Settings APIs ====================
606
+
607
+ Store.prototype.mcpGetSettings = function () {
608
+ const { store } = window
609
+ // Return safe settings (no sensitive data)
610
+ const config = store.config
611
+ const safeConfig = {
612
+ theme: config.theme,
613
+ language: config.language,
614
+ fontSize: config.fontSize,
615
+ fontFamily: config.fontFamily,
616
+ terminalType: config.terminalType,
617
+ cursorStyle: config.cursorStyle,
618
+ cursorBlink: config.cursorBlink,
619
+ scrollback: config.scrollback
620
+ }
621
+ return safeConfig
622
+ }
623
+
624
+ Store.prototype.mcpListTerminalThemes = function () {
625
+ const { store } = window
626
+ return store.terminalThemes.map(t => ({
627
+ id: t.id,
628
+ name: t.name,
629
+ themeLight: t.themeLight
630
+ }))
631
+ }
632
+
633
+ Store.prototype.mcpListUiThemes = function () {
634
+ const { store } = window
635
+ return (store.uiThemes || []).map(t => ({
636
+ id: t.id,
637
+ name: t.name
638
+ }))
639
+ }
640
+ }
@@ -25,6 +25,7 @@ import batchInputHistory from './batch-input-history'
25
25
  import transferExtend from './transfer-list'
26
26
  import addressBookmarkExtend from './address-bookmark'
27
27
  import widgetsExtend from './widgets'
28
+ import mcpHandlerExtend from './mcp-handler'
28
29
  import workspaceExtend from './workspace'
29
30
  import isColorDark from '../common/is-color-dark'
30
31
  import { getReverseColor } from '../common/reverse-color'
@@ -298,6 +299,7 @@ batchInputHistory(Store)
298
299
  transferExtend(Store)
299
300
  addressBookmarkExtend(Store)
300
301
  widgetsExtend(Store)
302
+ mcpHandlerExtend(Store)
301
303
  workspaceExtend(Store)
302
304
 
303
305
  export const StateStore = Store
@@ -16,6 +16,10 @@ export default Store => {
16
16
  }
17
17
 
18
18
  Store.prototype.runWidget = async (widgetId, config) => {
19
+ // If this is MCP server widget, initialize MCP handler first
20
+ if (widgetId === 'mcp-server') {
21
+ window.store.initMcpHandler()
22
+ }
19
23
  return window.pre.runGlobalAsync('runWidget', widgetId, config)
20
24
  }
21
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.3.166",
3
+ "version": "2.3.176",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",