@electerm/electerm-react 5.0.8 → 5.1.7

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 (41) hide show
  1. package/client/common/cache.js +2 -1
  2. package/client/common/constants.js +2 -0
  3. package/client/common/default-setting.js +10 -1
  4. package/client/common/left-sidebar-icon-defs.js +36 -0
  5. package/client/common/parse-json-safe.js +7 -3
  6. package/client/components/ai/ai-config-props.js +8 -0
  7. package/client/components/bookmark-form/common/fields.jsx +3 -2
  8. package/client/components/bookmark-form/common/ssh-agent.jsx +3 -2
  9. package/client/components/bookmark-form/common/x11.jsx +2 -2
  10. package/client/components/common/switch.jsx +63 -0
  11. package/client/components/common/switch.styl +7 -0
  12. package/client/components/footer/cmd-history.jsx +4 -8
  13. package/client/components/layout/layout.jsx +8 -5
  14. package/client/components/main/main.jsx +7 -2
  15. package/client/components/profile/profile-form-elem.jsx +2 -2
  16. package/client/components/quick-commands/quick-commands-form-elem.jsx +2 -2
  17. package/client/components/rdp/rdp-session.jsx +3 -4
  18. package/client/components/setting-panel/setting-common.jsx +9 -5
  19. package/client/components/setting-panel/setting-left-sidebar-icons.jsx +54 -0
  20. package/client/components/setting-panel/setting-terminal.jsx +3 -4
  21. package/client/components/setting-sync/data-import.jsx +3 -4
  22. package/client/components/setting-sync/setting-sync-form.jsx +3 -2
  23. package/client/components/sftp/file-compare-modal.jsx +1 -1
  24. package/client/components/sidebar/history.jsx +3 -8
  25. package/client/components/sidebar/index.jsx +22 -71
  26. package/client/components/sidebar/left-sidebar-icons.jsx +148 -0
  27. package/client/components/sidebar/transfer-list.jsx +1 -1
  28. package/client/components/spice/spice-session.jsx +4 -5
  29. package/client/components/terminal/shortcut-bar-defs.js +131 -0
  30. package/client/components/terminal/shortcut-bar-edit.jsx +205 -0
  31. package/client/components/terminal/shortcut-bar-entry.jsx +34 -0
  32. package/client/components/terminal/shortcut-bar.jsx +150 -0
  33. package/client/components/terminal/shortcut-bar.styl +186 -0
  34. package/client/components/terminal/terminal-color-query.mjs +47 -0
  35. package/client/components/terminal/terminal.jsx +39 -1
  36. package/client/components/terminal/terminal.styl +8 -0
  37. package/client/components/terminal-info/base.jsx +5 -7
  38. package/client/components/widgets/widget-form.jsx +3 -2
  39. package/client/store/init-state.js +3 -0
  40. package/client/store/mcp-handler.js +29 -1
  41. package/package.json +1 -1
@@ -0,0 +1,150 @@
1
+ /**
2
+ * shortcut-bar
3
+ *
4
+ * On touch devices, tapping a terminal pops the system input panel.
5
+ * Without a physical keyboard, modifier / function keys are hard to type, so
6
+ * this renders a slim, horizontally-scrollable bar pinned just outside the
7
+ * page content (its space is reserved by layout.jsx, so it never overlaps the
8
+ * terminal, footer or any other UI).
9
+ *
10
+ * - first (fixed) button collapses the bar
11
+ * - second (fixed) button opens the edit modal (shortcut-bar-edit.jsx)
12
+ * - tapping a shortcut sends it to the active terminal via runQuickCommand
13
+ *
14
+ * Only loaded on touch devices — see shortcut-bar-entry.jsx.
15
+ * Data + persistence live in shortcut-bar-defs.js.
16
+ */
17
+
18
+ import { auto } from 'manate/react'
19
+ import { useEffect, useState } from 'react'
20
+ import { createPortal } from 'react-dom'
21
+ import { DownOutlined, EditOutlined } from '@ant-design/icons'
22
+ import { refs } from '../common/ref'
23
+ import { shortcutBarHeight } from '../../common/constants'
24
+ import ShortcutBarEdit from './shortcut-bar-edit'
25
+ import { candidates, loadActive, saveActive } from './shortcut-bar-defs'
26
+ import './shortcut-bar.styl'
27
+
28
+ const e = window.translate
29
+
30
+ function sendToTerminal (data) {
31
+ const { store } = window
32
+ const term = refs.get('term-' + store.activeTabId)
33
+ if (term && typeof term.runQuickCommand === 'function') {
34
+ term.runQuickCommand(data, true)
35
+ }
36
+ }
37
+
38
+ function ShortcutBar (props) {
39
+ const { store } = props
40
+ const [buttons, setButtons] = useState(loadActive)
41
+ const [editing, setEditing] = useState(false)
42
+
43
+ // show the bar whenever a terminal becomes active / gets focused,
44
+ // hide it when the active tab is not a terminal.
45
+ useEffect(() => {
46
+ store.shortcutBarVisible = !!store.inActiveTerminal
47
+ }, [store.activeTabId, store.inActiveTerminal])
48
+
49
+ useEffect(() => {
50
+ function onTerminalFocus (ev) {
51
+ const el = ev.target
52
+ if (el && el.closest && el.closest('.term-wrap')) {
53
+ if (window.store.inActiveTerminal) {
54
+ window.store.shortcutBarVisible = true
55
+ }
56
+ }
57
+ }
58
+ document.addEventListener('focusin', onTerminalFocus)
59
+ document.addEventListener('pointerdown', onTerminalFocus)
60
+ return () => {
61
+ document.removeEventListener('focusin', onTerminalFocus)
62
+ document.removeEventListener('pointerdown', onTerminalFocus)
63
+ }
64
+ }, [])
65
+
66
+ // reserve layout space + lift the footer while the bar is shown.
67
+ // expose the bar height as a CSS var so the .styl + footer-lift rule stay
68
+ // in sync with the shortcutBarHeight constant consumed by layout.jsx.
69
+ useEffect(() => {
70
+ const on = !!store.shortcutBarVisible
71
+ document.body.classList.toggle('shortcut-bar-on', on)
72
+ if (on) {
73
+ document.documentElement.style.setProperty(
74
+ '--shortcut-bar-h',
75
+ shortcutBarHeight + 'px'
76
+ )
77
+ }
78
+ return () => {
79
+ document.body.classList.remove('shortcut-bar-on')
80
+ document.documentElement.style.removeProperty('--shortcut-bar-h')
81
+ }
82
+ }, [store.shortcutBarVisible])
83
+
84
+ function handleClose () {
85
+ store.shortcutBarVisible = false
86
+ }
87
+
88
+ function handleSave (next) {
89
+ setButtons(next)
90
+ saveActive(next)
91
+ setEditing(false)
92
+ }
93
+
94
+ function renderButton (b, i) {
95
+ return (
96
+ <button
97
+ type='button'
98
+ key={b.id + '-' + i}
99
+ className='shortcut-bar-btn'
100
+ onClick={() => sendToTerminal(b.data)}
101
+ >
102
+ {b.label}
103
+ </button>
104
+ )
105
+ }
106
+
107
+ if (!store.shortcutBarVisible) {
108
+ return null
109
+ }
110
+
111
+ return createPortal(
112
+ <div className='shortcut-bar' role='toolbar'>
113
+ <div className='shortcut-bar-fixed'>
114
+ <button
115
+ type='button'
116
+ className='shortcut-bar-icon-btn'
117
+ title={e('close')}
118
+ onClick={handleClose}
119
+ >
120
+ <DownOutlined />
121
+ </button>
122
+ <button
123
+ type='button'
124
+ className='shortcut-bar-icon-btn'
125
+ title={e('edit')}
126
+ onClick={() => setEditing(true)}
127
+ >
128
+ <EditOutlined />
129
+ </button>
130
+ </div>
131
+ <div className='shortcut-bar-scroll'>
132
+ {buttons.map(renderButton)}
133
+ </div>
134
+ {
135
+ editing && (
136
+ <ShortcutBarEdit
137
+ active={buttons}
138
+ candidates={candidates}
139
+ isMobile={store.isMobile}
140
+ onCancel={() => setEditing(false)}
141
+ onSave={handleSave}
142
+ />
143
+ )
144
+ }
145
+ </div>,
146
+ document.body
147
+ )
148
+ }
149
+
150
+ export default auto(ShortcutBar)
@@ -0,0 +1,186 @@
1
+ // shortcut-bar
2
+ // sits in reserved space at the bottom of the viewport (see layout.jsx +
3
+ // .shortcut-bar-on rule below). Height tracks --shortcut-bar-h which the
4
+ // component sets from the shortcutBarHeight constant.
5
+
6
+ .shortcut-bar
7
+ position fixed
8
+ left 0
9
+ right 0
10
+ bottom 0
11
+ height var(--shortcut-bar-h, 44px)
12
+ z-index 400
13
+ display flex
14
+ align-items stretch
15
+ background var(--main)
16
+ border-top 1px solid var(--main-lighter)
17
+ user-select none
18
+ -webkit-user-select none
19
+
20
+ .shortcut-bar-fixed
21
+ display flex
22
+ align-items stretch
23
+ flex-shrink 0
24
+ border-right 1px solid var(--main-lighter)
25
+
26
+ .shortcut-bar-icon-btn
27
+ width 44px
28
+ display flex
29
+ align-items center
30
+ justify-content center
31
+ background transparent
32
+ border none
33
+ color var(--text)
34
+ cursor pointer
35
+ font-size 16px
36
+ &:active
37
+ background var(--main-lighter)
38
+ &:hover
39
+ color var(--text-light)
40
+
41
+ .shortcut-bar-scroll
42
+ flex 1
43
+ display flex
44
+ align-items center
45
+ gap 6px
46
+ padding 0 8px
47
+ overflow-x auto
48
+ overflow-y hidden
49
+ scrollbar-width none
50
+ -webkit-overflow-scrolling touch
51
+ &::-webkit-scrollbar
52
+ display none
53
+
54
+ .shortcut-bar-btn
55
+ flex-shrink 0
56
+ height 32px
57
+ min-width 32px
58
+ padding 0 10px
59
+ display inline-flex
60
+ align-items center
61
+ justify-content center
62
+ border-radius 6px
63
+ border 1px solid var(--main-lighter)
64
+ background var(--main-light)
65
+ color var(--text)
66
+ font-size 13px
67
+ cursor pointer
68
+ white-space nowrap
69
+ &:active
70
+ background var(--primary)
71
+ color var(--primary-contrast)
72
+ &:hover
73
+ color var(--text-light)
74
+
75
+ // while the bar is visible, lift the footer + reserve space so nothing is
76
+ // covered (terminal area is shrunk by layout.jsx via store.shortcutBarVisible)
77
+ .shortcut-bar-on
78
+ .main-footer
79
+ bottom var(--shortcut-bar-h, 44px)
80
+
81
+ // --- edit modal -----------------------------------------------------------
82
+
83
+ .shortcut-edit-wrap
84
+ .custom-modal-content
85
+ max-width 92vw
86
+ .shortcut-edit-section
87
+ margin-bottom 16px
88
+ &:last-child
89
+ margin-bottom 0
90
+ .shortcut-edit-section-title
91
+ margin-bottom 8px
92
+ color var(--text)
93
+ font-weight bold
94
+ font-size 13px
95
+
96
+ .shortcut-active-list
97
+ display flex
98
+ flex-wrap wrap
99
+ gap 6px
100
+ min-height 48px
101
+ padding 8px
102
+ border 1px dashed var(--main-lighter)
103
+ border-radius 6px
104
+ background var(--main-dark)
105
+ .shortcut-active-empty
106
+ color var(--text-dark)
107
+ font-size 12px
108
+ width 100%
109
+ text-align center
110
+ line-height 32px
111
+
112
+ .shortcut-active-item
113
+ display inline-flex
114
+ align-items center
115
+ gap 4px
116
+ height 30px
117
+ padding 0 4px 0 10px
118
+ border-radius 6px
119
+ background var(--main-light)
120
+ border 1px solid var(--main-lighter)
121
+ color var(--text)
122
+ font-size 13px
123
+ .shortcut-active-label
124
+ white-space nowrap
125
+ .shortcut-active-remove
126
+ display flex
127
+ align-items center
128
+ justify-content center
129
+ width 20px
130
+ height 20px
131
+ border none
132
+ background transparent
133
+ color var(--text-dark)
134
+ border-radius 4px
135
+ cursor pointer
136
+ font-size 11px
137
+ &:hover
138
+ color var(--error)
139
+ background var(--main-lighter)
140
+
141
+ .shortcut-candidate-list
142
+ display flex
143
+ flex-wrap wrap
144
+ gap 6px
145
+ max-height 180px
146
+ overflow-y auto
147
+ padding 4px
148
+ .shortcut-candidate-item
149
+ height 30px
150
+ min-width 34px
151
+ padding 0 10px
152
+ display inline-flex
153
+ align-items center
154
+ justify-content center
155
+ border-radius 6px
156
+ border 1px solid var(--main-lighter)
157
+ background var(--main)
158
+ color var(--text)
159
+ font-size 13px
160
+ cursor pointer
161
+ &:active
162
+ background var(--main-lighter)
163
+ &:hover
164
+ color var(--text-light)
165
+ &.added
166
+ opacity 0.45
167
+ cursor default
168
+
169
+ .shortcut-custom-form
170
+ display flex
171
+ gap 8px
172
+ align-items center
173
+ flex-wrap wrap
174
+ .shortcut-search-input
175
+ flex 1
176
+ min-width 160px
177
+ .shortcut-custom-input
178
+ flex 1
179
+ min-width 120px
180
+ .shortcut-custom-add
181
+ height 32px
182
+ white-space nowrap
183
+
184
+ .shortcut-edit-footer
185
+ justify-content space-between
186
+ width 100%
@@ -104,6 +104,53 @@ export function colorToOscRgb (color) {
104
104
  return `rgb:${rgb.map(toHexByte).join('/')}`
105
105
  }
106
106
 
107
+ /**
108
+ * Blend a (possibly translucent) selection color over an opaque background,
109
+ * producing the colour in xterm's internal format ({ css, rgba } where
110
+ * rgba = r<<24 | g<<16 | b<<8 | a).
111
+ *
112
+ * xterm derives its DOM selection colour as blend(theme.background,
113
+ * selectionBackground). When the terminal background is forced transparent
114
+ * (DOM renderer keeps the real background behind via CSS), that blend runs
115
+ * over transparent-black and a light selection turns into dark gray. This
116
+ * helper recomputes it over the real visible background instead.
117
+ *
118
+ * `selectionColor` is an xterm color object ({ css, rgba }) e.g.
119
+ * colors.selectionBackgroundTransparent; `visibleBackground` is an opaque
120
+ * colour string.
121
+ */
122
+ export function blendSelectionOverBackground (visibleBackground, selectionColor) {
123
+ const bgRgb = parseColorToRgb(visibleBackground)
124
+ const sel = selectionColor?.rgba
125
+ if (!bgRgb || typeof sel !== 'number') {
126
+ return null
127
+ }
128
+ const [br, bg, bb] = bgRgb
129
+ const a = (sel & 255) / 255
130
+ if (a <= 0) {
131
+ return null
132
+ }
133
+ const toCss = (r, g, b, alpha) =>
134
+ `#${[r, g, b, alpha].map(toHexByte).join('')}`
135
+ const sr = sel >> 24 & 255
136
+ const sg = sel >> 16 & 255
137
+ const sb = sel >> 8 & 255
138
+ if (a >= 1) {
139
+ return {
140
+ css: toCss(sr, sg, sb, 255),
141
+ rgba: ((sel & 0xffffff) | 0xff000000) >>> 0
142
+ }
143
+ }
144
+ const r = br + Math.round((sr - br) * a)
145
+ const g = bg + Math.round((sg - bg) * a)
146
+ const b = bb + Math.round((sb - bb) * a)
147
+ const alpha = Math.round(a * 255)
148
+ return {
149
+ css: toCss(r, g, b, alpha),
150
+ rgba: (r << 24 | g << 16 | b << 8 | alpha) >>> 0
151
+ }
152
+ }
153
+
107
154
  export function buildOscColorResponse (identifier, color, fallbackColor) {
108
155
  const oscColor = colorToOscRgb(color) || colorToOscRgb(fallbackColor)
109
156
  return oscColor
@@ -71,7 +71,8 @@ import {
71
71
  } from './xterm-loader.js'
72
72
  import {
73
73
  createRendererThemeConfig,
74
- handleTerminalColorQuery
74
+ handleTerminalColorQuery,
75
+ blendSelectionOverBackground
75
76
  } from './terminal-color-query.mjs'
76
77
 
77
78
  const e = window.translate
@@ -1456,6 +1457,7 @@ class Term extends Component {
1456
1457
  return
1457
1458
  }
1458
1459
  term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
1460
+ this.fixSelectionColors(term)
1459
1461
  term.refresh(0, term.rows - 1)
1460
1462
  if (deferred && this.props.config.rendererType === rendererTypes.webGL) {
1461
1463
  window.cancelAnimationFrame(this.timers.themeRaf)
@@ -1464,11 +1466,46 @@ class Term extends Component {
1464
1466
  return
1465
1467
  }
1466
1468
  this.term.options.theme = this.getRendererThemeConfig(this.props.themeConfig)
1469
+ this.fixSelectionColors(this.term)
1467
1470
  this.term.refresh(0, this.term.rows - 1)
1468
1471
  })
1469
1472
  }
1470
1473
  }
1471
1474
 
1475
+ /**
1476
+ * xterm computes its DOM selection colour as blend(theme.background,
1477
+ * selectionBackground). The DOM renderer keeps the terminal background
1478
+ * transparent (rgba(0,0,0,0)) so electerm's own CSS background / image
1479
+ * shows through, which makes a light selection blend over transparent-black
1480
+ * and render as dark gray. Recompute the selection over the real visible
1481
+ * background so terminal:selectionBackground=rgba(...) is applied as the
1482
+ * configured semi-transparent colour.
1483
+ */
1484
+ fixSelectionColors = (term) => {
1485
+ const themeService = term?._core?._themeService
1486
+ if (!themeService?.modifyColors) {
1487
+ return
1488
+ }
1489
+ const visibleBackground = this.getVisibleTerminalBackground()
1490
+ const colors = themeService.colors
1491
+ themeService.modifyColors((c) => {
1492
+ const active = blendSelectionOverBackground(
1493
+ visibleBackground,
1494
+ colors.selectionBackgroundTransparent
1495
+ )
1496
+ if (active) {
1497
+ c.selectionBackgroundOpaque = active
1498
+ }
1499
+ const inactive = blendSelectionOverBackground(
1500
+ visibleBackground,
1501
+ colors.selectionInactiveBackgroundTransparent
1502
+ )
1503
+ if (inactive) {
1504
+ c.selectionInactiveBackgroundOpaque = inactive
1505
+ }
1506
+ })
1507
+ }
1508
+
1472
1509
  initTerminal = async () => {
1473
1510
  const { themeConfig, tab = {}, config = {} } = this.props
1474
1511
  const tc = this.getRendererThemeConfig(themeConfig)
@@ -1492,6 +1529,7 @@ class Term extends Component {
1492
1529
  term.open(this.domRef.current, true)
1493
1530
  this.registerTerminalColorQueryHandlers(term, themeConfig)
1494
1531
  await this.loadRenderer(term, config)
1532
+ this.fixSelectionColors(term)
1495
1533
  // Re-apply the theme after the renderer is loaded. The Terminal was
1496
1534
  // constructed before UiTheme's useEffect ran, so the initial theme
1497
1535
  // may have a stale background. Pass `term` directly because this.term
@@ -34,6 +34,14 @@
34
34
  background transparent
35
35
  .xterm-viewport
36
36
  background-color transparent !important
37
+ // xterm's DOM renderer paints the background of selected text cells directly
38
+ // on the <span> as an opaque 6-digit hex (dropping alpha), while empty cells
39
+ // use the translucent .xterm-selection overlay behind the rows. xterm marks
40
+ // those spans with .xterm-decoration-top ("rendered above the selection"),
41
+ // so neutralise that inline background to let the corrected selection overlay
42
+ // show through uniformly across both empty and text cells.
43
+ .xterm-decoration-top
44
+ background-color transparent !important
37
45
  .xterm-screen::before
38
46
  content ''
39
47
  position absolute
@@ -3,10 +3,10 @@
3
3
  */
4
4
  import { Component } from 'react'
5
5
  import {
6
- Switch,
7
6
  Space,
8
7
  Button
9
8
  } from 'antd'
9
+ import SwitchLabel from '../common/switch'
10
10
  import defaults from '../../common/default-setting'
11
11
  import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
12
12
  import {
@@ -123,9 +123,8 @@ export default class TerminalInfoBase extends Component {
123
123
  }
124
124
  const name = e('addTimeStampToTermLog')
125
125
  return (
126
- <Switch
127
- checkedChildren={name}
128
- unCheckedChildren={name}
126
+ <SwitchLabel
127
+ label={name}
129
128
  checked={addTimeStampToTermLog}
130
129
  onChange={this.handleToggleTimestamp}
131
130
  className='mg1b'
@@ -176,9 +175,8 @@ export default class TerminalInfoBase extends Component {
176
175
  <b>ID:</b> {id}
177
176
  </div>
178
177
  <div className='pd1b'>
179
- <Switch
180
- checkedChildren={name}
181
- unCheckedChildren={name}
178
+ <SwitchLabel
179
+ label={name}
182
180
  checked={saveTerminalLogToFile}
183
181
  onChange={this.handleToggle}
184
182
  className='mg1r mg1b'
@@ -2,8 +2,9 @@
2
2
  * Widget form component
3
3
  */
4
4
  import React, { useState, useEffect } from 'react'
5
- import { Form, Input, InputNumber, Switch, Select, Button, Tooltip, Alert, Space } from 'antd'
5
+ import { Form, Input, InputNumber, Select, Button, Tooltip, Alert, Space } from 'antd'
6
6
  import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
7
+ import SwitchLabel from '../common/switch'
7
8
  import HelpIcon from '../common/help-icon'
8
9
  import { nanoid } from 'nanoid'
9
10
  import BatchOpEditor from '../batch-op/batch-op-editor'
@@ -91,7 +92,7 @@ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInsta
91
92
  valuePropName='checked'
92
93
  tooltip={description}
93
94
  >
94
- <Switch />
95
+ <SwitchLabel />
95
96
  </Form.Item>
96
97
  )
97
98
  default:
@@ -167,6 +167,9 @@ export default () => {
167
167
  pinnedQuickCommandBar: ls.getItem(pinnedQuickCommandBarKey) === 'y',
168
168
  qmSortByFrequency: ls.getItem(qmSortByFrequencyKey) === 'yes',
169
169
 
170
+ // touch-only shortcut bar (see components/terminal/shortcut-bar-entry.jsx)
171
+ shortcutBarVisible: false,
172
+
170
173
  // sidebar
171
174
  openedSideBar: ls.getItem(openedSidebarKey) || '',
172
175
  _leftSidePanelWidth: parseInt(ls.getItem(leftSidePanelWidthKey), 10) || 300,
@@ -18,6 +18,30 @@ import {
18
18
  } from '../components/bookmark-form/fix-bookmark-default'
19
19
  import newTerm from '../common/new-terminal'
20
20
 
21
+ // Dangerous props that must not be accepted from MCP/AI tool calls.
22
+ // Mirrors the blocklist in src/client/store/tab.js (dangerousTabProps).
23
+ // These allow arbitrary command execution if set (e.g. execLinux/execLinuxArgs
24
+ // override the shell binary, setEnv injects environment variables, runScripts
25
+ // executes scripts, interactiveValues injects interactive prompts).
26
+ const dangerousTabProps = [
27
+ 'execLinux',
28
+ 'execMac',
29
+ 'execWindows',
30
+ 'execWindowsArgs',
31
+ 'execMacArgs',
32
+ 'execLinuxArgs',
33
+ 'setEnv',
34
+ 'runScripts',
35
+ 'interactiveValues'
36
+ ]
37
+
38
+ // Strip dangerous props from an object, returning a safe copy.
39
+ function stripDangerousTabProps (obj) {
40
+ return Object.fromEntries(
41
+ Object.entries(obj).filter(([key]) => !dangerousTabProps.includes(key))
42
+ )
43
+ }
44
+
21
45
  export default Store => {
22
46
  // Initialize MCP handler - called when MCP widget is started
23
47
  Store.prototype.initMcpHandler = function () {
@@ -497,7 +521,11 @@ export default Store => {
497
521
 
498
522
  Store.prototype.mcpOpenTab = function (args) {
499
523
  const { store } = window
500
- const data = fixBookmarkData({ ...args })
524
+ // Strip dangerous execution-related props before any processing.
525
+ // This prevents MCP/AI callers from injecting execLinux/execLinuxArgs,
526
+ // setEnv, runScripts, etc. to spawn arbitrary local processes.
527
+ const safeArgs = stripDangerousTabProps({ ...args })
528
+ const data = fixBookmarkData(safeArgs)
501
529
 
502
530
  const { valid, errors } = validateBookmarkData(data)
503
531
  if (!valid) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "5.0.8",
3
+ "version": "5.1.7",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",