@electerm/electerm-react 2.13.6 → 2.16.6

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 (59) hide show
  1. package/client/components/ai/ai-chat.jsx +44 -2
  2. package/client/components/ai/ai-stop-icon.jsx +13 -0
  3. package/client/components/ai/ai.styl +10 -0
  4. package/client/components/bg/css-overwrite.jsx +158 -187
  5. package/client/components/bg/custom-css.jsx +9 -15
  6. package/client/components/bookmark-form/bookmark-schema.js +7 -1
  7. package/client/components/bookmark-form/common/color-picker.jsx +4 -8
  8. package/client/components/bookmark-form/common/exec-settings-field.jsx +44 -0
  9. package/client/components/bookmark-form/common/fields.jsx +3 -0
  10. package/client/components/bookmark-form/config/common-fields.js +1 -0
  11. package/client/components/bookmark-form/config/local.js +3 -1
  12. package/client/components/common/animate-text.jsx +22 -23
  13. package/client/components/common/modal.jsx +2 -0
  14. package/client/components/common/notification.jsx +1 -1
  15. package/client/components/common/opacity.jsx +8 -6
  16. package/client/components/common/password.jsx +19 -32
  17. package/client/components/footer/cmd-history.jsx +154 -0
  18. package/client/components/footer/cmd-history.styl +73 -0
  19. package/client/components/footer/footer-entry.jsx +15 -1
  20. package/client/components/main/main.jsx +2 -3
  21. package/client/components/main/ui-theme.jsx +10 -6
  22. package/client/components/profile/profile-list.jsx +1 -1
  23. package/client/components/quick-commands/quick-commands-list.jsx +1 -1
  24. package/client/components/quick-commands/quick-commands-select.jsx +1 -4
  25. package/client/components/rdp/rdp-session.jsx +23 -4
  26. package/client/components/session/session.styl +1 -3
  27. package/client/components/setting-panel/list.styl +7 -0
  28. package/client/components/setting-panel/terminal-bg-config.jsx +2 -0
  29. package/client/components/setting-panel/text-bg-modal.jsx +9 -9
  30. package/client/components/setting-sync/setting-sync-form.jsx +10 -5
  31. package/client/components/sftp/file-item.jsx +22 -0
  32. package/client/components/sidebar/history-item.jsx +6 -3
  33. package/client/components/sidebar/history.jsx +48 -5
  34. package/client/components/sidebar/sidebar-panel.jsx +0 -13
  35. package/client/components/sidebar/sidebar.styl +19 -0
  36. package/client/components/ssh-config/load-ssh-configs-item.jsx +99 -0
  37. package/client/components/ssh-config/load-ssh-configs.jsx +38 -9
  38. package/client/components/ssh-config/ssh-config.styl +3 -0
  39. package/client/components/tabs/add-btn-menu.jsx +28 -4
  40. package/client/components/tabs/add-btn.jsx +1 -1
  41. package/client/components/tabs/add-btn.styl +8 -0
  42. package/client/components/terminal/terminal.jsx +28 -11
  43. package/client/components/terminal/transfer-client-base.js +44 -0
  44. package/client/components/terminal/trzsz-client.js +10 -11
  45. package/client/components/terminal/zmodem-client.js +10 -11
  46. package/client/components/text-editor/edit-with-custom-editor.jsx +49 -0
  47. package/client/components/text-editor/simple-editor.jsx +38 -6
  48. package/client/components/text-editor/text-editor-form.jsx +13 -5
  49. package/client/components/text-editor/text-editor.jsx +20 -1
  50. package/client/components/vnc/vnc-session.jsx +3 -0
  51. package/client/components/vnc/vnc.styl +1 -1
  52. package/client/store/bookmark.js +3 -11
  53. package/client/store/common.js +31 -4
  54. package/client/store/init-state.js +26 -1
  55. package/client/store/store.js +1 -1
  56. package/client/store/sync.js +2 -3
  57. package/client/store/watch.js +8 -1
  58. package/package.json +1 -1
  59. package/client/components/ssh-config/ssh-config-item.jsx +0 -24
@@ -3,15 +3,13 @@ import {
3
3
  Input,
4
4
  InputNumber,
5
5
  Space,
6
- Typography,
7
6
  Select,
8
- Button
7
+ Button,
8
+ Modal
9
9
  } from 'antd'
10
- import Modal from '../common/modal'
11
10
  import { ColorPicker } from '../bookmark-form/common/color-picker.jsx'
12
11
 
13
12
  const { TextArea } = Input
14
- const { Title } = Typography
15
13
  const e = window.translate
16
14
 
17
15
  export default function TextBgModal ({
@@ -68,9 +66,9 @@ export default function TextBgModal ({
68
66
  footer={footer}
69
67
  >
70
68
  <div className='pd1'>
71
- <Space direction='vertical' size='large' style={{ width: '100%' }}>
69
+ <Space orientation='vertical' size='large' className='width-100'>
72
70
  <div>
73
- <Title level={5}>{e('text')}</Title>
71
+ <b>{e('text')}</b>
74
72
  <TextArea
75
73
  value={text}
76
74
  onChange={(e) => setText(e.target.value)}
@@ -81,7 +79,7 @@ export default function TextBgModal ({
81
79
  </div>
82
80
 
83
81
  <div>
84
- <Title level={5}>{e('fontSize')}</Title>
82
+ <b>{e('fontSize')}</b>
85
83
  <InputNumber
86
84
  value={fontSize}
87
85
  onChange={setFontSize}
@@ -93,7 +91,7 @@ export default function TextBgModal ({
93
91
  </div>
94
92
 
95
93
  <div>
96
- <Title level={5}>{e('textColor')}</Title>
94
+ <b>{e('textColor')}</b>
97
95
  <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
98
96
  <ColorPicker
99
97
  value={color}
@@ -109,7 +107,7 @@ export default function TextBgModal ({
109
107
  </div>
110
108
 
111
109
  <div>
112
- <Title level={5}>{e('fontFamily')}</Title>
110
+ <b>{e('fontFamily')}</b>
113
111
  <Select
114
112
  value={fontFamily}
115
113
  onChange={setFontFamily}
@@ -140,3 +138,5 @@ export default function TextBgModal ({
140
138
  </Modal>
141
139
  )
142
140
  }
141
+
142
+ TextBgModal.displayName = 'TextBgModal'
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * bookmark form
7
7
  */
8
- import { useDelta, useConditionalEffect } from 'react-delta-hooks'
8
+ import { useEffect, useRef } from 'react'
9
9
  import { ArrowDownOutlined, ArrowUpOutlined, SaveOutlined, ClearOutlined } from '@ant-design/icons'
10
10
  import { Button, Input, Form, Alert } from 'antd'
11
11
  import { notification } from '../common/notification'
@@ -27,10 +27,15 @@ function trim (str) {
27
27
 
28
28
  export default function SyncForm (props) {
29
29
  const [form] = Form.useForm()
30
- const delta = useDelta(props.formData)
31
- useConditionalEffect(() => {
32
- form.resetFields()
33
- }, delta && delta.prev && !eq(delta.prev, delta.curr))
30
+ const prevRef = useRef(null)
31
+
32
+ useEffect(() => {
33
+ if (prevRef.current && !eq(prevRef.current, props.formData)) {
34
+ form.resetFields()
35
+ }
36
+ prevRef.current = props.formData
37
+ }, [props.formData])
38
+
34
39
  const { syncType } = props
35
40
  function disabled () {
36
41
  if (syncType === syncTypes.cloud) {
@@ -651,6 +651,28 @@ export default class FileSection extends React.Component {
651
651
  this.watchFile(tempPath)
652
652
  }
653
653
 
654
+ editWithCustomEditor = async (text, editorCommand) => {
655
+ const {
656
+ path,
657
+ name,
658
+ type
659
+ } = this.state.file
660
+ let tempPath = ''
661
+ if (type === typeMap.local) {
662
+ tempPath = window.pre.resolve(path, name)
663
+ } else {
664
+ const id = generate()
665
+ tempPath = window.pre.resolve(
666
+ window.pre.tempDir, `electerm-temp-${id}-${name}`
667
+ )
668
+ await fs.writeFile(tempPath, text)
669
+ }
670
+ this.watchingFile = tempPath
671
+ window.pre.runGlobalAsync('watchFile', tempPath)
672
+ await window.pre.runGlobalAsync('openFileWithEditor', tempPath, editorCommand)
673
+ window.pre.ipcOnEvent('file-change', this.onFileChange)
674
+ }
675
+
654
676
  onFileChange = (e, text) => {
655
677
  this.editor.editWithSystemEditorDone({
656
678
  id: this.id,
@@ -6,8 +6,7 @@ import { refsStatic } from '../common/ref'
6
6
  export default function HistoryItem (props) {
7
7
  const { store } = window
8
8
  const {
9
- item,
10
- index
9
+ item
11
10
  } = props
12
11
  const timeoutRef = useRef(null)
13
12
 
@@ -30,7 +29,11 @@ export default function HistoryItem (props) {
30
29
 
31
30
  function handleDelete (e) {
32
31
  e.stopPropagation()
33
- store.history.splice(index, 1)
32
+ const { id } = item
33
+ const i = store.history.findIndex((i) => i.id === id)
34
+ if (i !== -1) {
35
+ store.history.splice(i, 1)
36
+ }
34
37
  }
35
38
 
36
39
  function handleBookmark (e) {
@@ -2,31 +2,74 @@
2
2
  * history select
3
3
  */
4
4
 
5
+ import React, { useState, useEffect } from 'react'
5
6
  import { auto } from 'manate/react'
7
+ import { Switch } from 'antd'
8
+ import { UnorderedListOutlined } from '@ant-design/icons'
6
9
  import HistoryItem from './history-item'
10
+ import { getItemJSON, setItemJSON } from '../../common/safe-local-storage.js'
11
+
12
+ const SORT_BY_FREQ_KEY = 'electerm-history-sort-by-frequency'
7
13
 
8
14
  export default auto(function HistoryPanel (props) {
9
15
  const { store } = window
10
16
  if (store.config.disableConnectionHistory) {
11
17
  return null
12
18
  }
19
+ const [sortByFrequency, setSortByFrequency] = useState(() => {
20
+ return getItemJSON(SORT_BY_FREQ_KEY, false)
21
+ })
22
+
23
+ useEffect(() => {
24
+ setItemJSON(SORT_BY_FREQ_KEY, sortByFrequency)
25
+ }, [sortByFrequency])
26
+
13
27
  const {
14
28
  history
15
29
  } = store
16
- const arr = props.sort
17
- ? [...history].sort((a, b) => { return b.count - a.count })
18
- : history
30
+ let arr = [...history]
31
+ if (sortByFrequency) {
32
+ arr = arr.sort((a, b) => { return b.count - a.count })
33
+ }
34
+
35
+ const handleSortByFrequencyChange = (checked) => {
36
+ setSortByFrequency(checked)
37
+ }
38
+
39
+ const handleClearHistory = () => {
40
+ store.clearHistory()
41
+ }
42
+ const e = window.translate
43
+ const switchProps = {
44
+ checkedChildren: e('sortByFrequency'),
45
+ unCheckedChildren: e('sortByFrequency'),
46
+ checked: sortByFrequency,
47
+ onChange: handleSortByFrequencyChange,
48
+ size: 'small'
49
+ }
50
+ const clearIconProps = {
51
+ className: 'history-clear-icon pointer clear-ai-icon icon-hover',
52
+ title: window.translate('clear'),
53
+ onClick: handleClearHistory
54
+ }
19
55
  return (
20
56
  <div
21
57
  className='sidebar-panel-history'
22
58
  >
23
- <div className='pd2x'>
59
+ <div className='history-header pd2x pd2b'>
60
+ <Switch
61
+ {...switchProps}
62
+ />
63
+ <UnorderedListOutlined
64
+ {...clearIconProps}
65
+ />
66
+ </div>
67
+ <div className='history-body'>
24
68
  {
25
69
  arr.map((item, i) => {
26
70
  return (
27
71
  <HistoryItem
28
72
  key={item.id}
29
- index={i}
30
73
  item={item}
31
74
  />
32
75
  )
@@ -13,7 +13,6 @@ import {
13
13
  PlusCircleOutlined,
14
14
  ShrinkOutlined,
15
15
  PushpinOutlined,
16
- UnorderedListOutlined,
17
16
  SelectOutlined
18
17
  } from '@ant-design/icons'
19
18
 
@@ -29,21 +28,9 @@ export default memo(function SidebarPanel (props) {
29
28
  const prps1 = {
30
29
  className: prps.className + (pinned ? ' pinned' : '')
31
30
  }
32
- const props2 = {
33
- onClick: store.clearHistory,
34
- className: 'mg2x pointer clear-ai-icon icon-hover'
35
- }
36
- const tabBarExtraContent = sidebarPanelTab === 'history'
37
- ? (
38
- <UnorderedListOutlined
39
- {...props2}
40
- />
41
- )
42
- : null
43
31
  const tabsProps = {
44
32
  activeKey: sidebarPanelTab,
45
33
  onChange: store.handleSidebarPanelTab,
46
- tabBarExtraContent,
47
34
  items: [
48
35
  {
49
36
  key: 'bookmarks',
@@ -48,6 +48,25 @@
48
48
  flex-direction column
49
49
  overflow hidden
50
50
  min-height 0
51
+ .history-header
52
+ flex-shrink 0
53
+ display flex
54
+ align-items center
55
+ border-bottom 1px solid var(--border)
56
+ position sticky
57
+ top 0
58
+ z-index 10
59
+ background var(--main)
60
+ .history-clear-icon
61
+ margin-left auto
62
+ margin-right 0
63
+ color var(--text-dark)
64
+ &:hover
65
+ color var(--error)
66
+ .history-body
67
+ flex 1
68
+ overflow-y auto
69
+ overflow-x hidden
51
70
  .not-system-ui.is-mac
52
71
  .sidebar-bar
53
72
  margin-top 20px
@@ -0,0 +1,99 @@
1
+ import { useState } from 'react'
2
+ import {
3
+ Input
4
+ } from 'antd'
5
+ import {
6
+ EditOutlined,
7
+ DeleteOutlined,
8
+ CheckOutlined,
9
+ CloseOutlined
10
+ } from '@ant-design/icons'
11
+
12
+ const { TextArea } = Input
13
+
14
+ export default function LoadSshConfigsItem (props) {
15
+ const { item, index, onDelete, onUpdate } = props
16
+ const [isEditing, setIsEditing] = useState(false)
17
+ const [editValue, setEditValue] = useState(JSON.stringify(item, null, 2))
18
+
19
+ const handleToggleEdit = function () {
20
+ if (isEditing) {
21
+ try {
22
+ const parsed = JSON.parse(editValue)
23
+ onUpdate(index, parsed)
24
+ } catch (err) {
25
+ console.error('Invalid JSON:', err)
26
+ setEditValue(JSON.stringify(item, null, 2))
27
+ }
28
+ } else {
29
+ setEditValue(JSON.stringify(item, null, 2))
30
+ }
31
+ setIsEditing(!isEditing)
32
+ }
33
+
34
+ const handleDelete = function () {
35
+ onDelete(index)
36
+ }
37
+
38
+ const handleCancelEdit = function () {
39
+ setEditValue(JSON.stringify(item, null, 2))
40
+ setIsEditing(false)
41
+ }
42
+
43
+ function renderActions () {
44
+ if (isEditing) {
45
+ return [
46
+ <CheckOutlined
47
+ className='mg1r pointer icon-success'
48
+ onClick={handleToggleEdit}
49
+ key='confirm-ssh-config-item'
50
+ />,
51
+ <CloseOutlined
52
+ className='mg1r pointer icon-warning'
53
+ onClick={handleCancelEdit}
54
+ key='cancel-ssh-config-item'
55
+ />
56
+ ]
57
+ }
58
+ return [
59
+ <EditOutlined
60
+ className='mg1r pointer ssh-config-item-edit-icon'
61
+ onClick={handleToggleEdit}
62
+ key='edit-ssh-config-item'
63
+ />,
64
+ <DeleteOutlined
65
+ className='pointer icon-danger ssh-config-item-delete-icon'
66
+ onClick={handleDelete}
67
+ key='del-ssh-config-item'
68
+ />
69
+ ]
70
+ }
71
+
72
+ function renderContent () {
73
+ if (isEditing) {
74
+ return (
75
+ <TextArea
76
+ value={editValue}
77
+ onChange={(e) => setEditValue(e.target.value)}
78
+ rows={10}
79
+ className='mg1t'
80
+ />
81
+ )
82
+ }
83
+ return (
84
+ <pre className='ssh-config-item-content'>
85
+ {JSON.stringify(item, null, 2)}
86
+ </pre>
87
+ )
88
+ }
89
+
90
+ return (
91
+ <div className='ssh-config-item pd1'>
92
+ <div className='pd1b ssh-config-item-header'>
93
+ <b className='mg1r'>[{index + 1}]</b>
94
+ {renderActions()}
95
+ </div>
96
+ {renderContent()}
97
+ </div>
98
+ )
99
+ }
@@ -5,18 +5,20 @@ import {
5
5
  Empty
6
6
  } from 'antd'
7
7
  import { useState, useEffect } from 'react'
8
- import SshConfigItem from './ssh-config-item'
9
8
  import * as ls from '../../common/safe-local-storage'
10
9
  import {
11
10
  sshConfigLoadKey
12
11
  } from '../../common/constants'
13
12
  import { ReloadOutlined } from '@ant-design/icons'
13
+ import LoadSshConfigsItem from './load-ssh-configs-item'
14
+ import './ssh-config.styl'
14
15
 
15
16
  const e = window.translate
16
17
 
17
18
  export default function LoadSshConfigs (props) {
18
19
  const [loading, setLoading] = useState(false)
19
20
  const { sshConfigs } = props
21
+ const [localConfigs, setLocalConfigs] = useState([])
20
22
 
21
23
  const {
22
24
  store
@@ -24,6 +26,11 @@ export default function LoadSshConfigs (props) {
24
26
  const {
25
27
  showSshConfigModal
26
28
  } = props
29
+
30
+ useEffect(() => {
31
+ setLocalConfigs(sshConfigs)
32
+ }, [sshConfigs])
33
+
27
34
  const handleCancel = function () {
28
35
  store.showSshConfigModal = false
29
36
  }
@@ -35,21 +42,43 @@ export default function LoadSshConfigs (props) {
35
42
 
36
43
  const handleLoadSshConfig = function () {
37
44
  store.showSshConfigModal = false
38
- store.addSshConfigs(sshConfigs)
45
+ store.addSshConfigs(localConfigs)
39
46
  ls.setItem(sshConfigLoadKey, 'yes')
40
47
  }
41
48
 
49
+ const handleDeleteItem = function (index) {
50
+ const newConfigs = [...localConfigs]
51
+ newConfigs.splice(index, 1)
52
+ setLocalConfigs(newConfigs)
53
+ }
54
+
55
+ const handleUpdateItem = function (index, newItem) {
56
+ const newConfigs = [...localConfigs]
57
+ newConfigs[index] = newItem
58
+ setLocalConfigs(newConfigs)
59
+ }
60
+
42
61
  const renderList = function () {
43
- if (!sshConfigs.length) {
62
+ if (!localConfigs.length) {
44
63
  return (
45
64
  <Empty />
46
65
  )
47
66
  }
48
- return sshConfigs.map((d, i) => {
49
- return (
50
- <SshConfigItem item={d} key={d.title} />
51
- )
52
- })
67
+ return (
68
+ <div className='pd1b ssh-config-list'>
69
+ {
70
+ localConfigs.map((item, index) => (
71
+ <LoadSshConfigsItem
72
+ key={index}
73
+ item={item}
74
+ index={index}
75
+ onDelete={handleDeleteItem}
76
+ onUpdate={handleUpdateItem}
77
+ />
78
+ ))
79
+ }
80
+ </div>
81
+ )
53
82
  }
54
83
 
55
84
  useEffect(() => {
@@ -89,7 +118,7 @@ export default function LoadSshConfigs (props) {
89
118
  type='primary'
90
119
  className='mg1r mg1b'
91
120
  onClick={handleLoadSshConfig}
92
- disabled={!sshConfigs.length || loading}
121
+ disabled={!localConfigs.length || loading}
93
122
  >
94
123
  {e('import')}
95
124
  </Button>
@@ -0,0 +1,3 @@
1
+ .ssh-config-list
2
+ max-height 60vh
3
+ overflow auto
@@ -2,13 +2,15 @@
2
2
  * Add button menu component
3
3
  */
4
4
 
5
- import React, { useCallback } from 'react'
5
+ import React, { useCallback, useState } from 'react'
6
+ import { Tabs } from 'antd'
6
7
  import {
7
8
  CodeFilled,
8
9
  RightSquareFilled,
9
10
  RobotOutlined
10
11
  } from '@ant-design/icons'
11
12
  import BookmarksList from '../sidebar/bookmark-select'
13
+ import History from '../sidebar/history'
12
14
  import DragHandle from '../common/drag-handle'
13
15
  import QuickConnect from './quick-connect'
14
16
 
@@ -26,6 +28,7 @@ export default function AddBtnMenu ({
26
28
  setAddPanelWidth
27
29
  }) {
28
30
  const { onNewSsh, onNewSshAI } = window.store
31
+ const [activeTab, setActiveTab] = useState('bookmarks')
29
32
  const cls = 'pd2x pd1y context-item pointer'
30
33
  const addTabBtn = window.store.hasNodePty
31
34
  ? (
@@ -59,6 +62,24 @@ export default function AddBtnMenu ({
59
62
  left: menuPosition === 'right'
60
63
  }
61
64
 
65
+ const tabItems = [
66
+ {
67
+ key: 'bookmarks',
68
+ label: e('bookmarks')
69
+ },
70
+ {
71
+ key: 'history',
72
+ label: e('history')
73
+ }
74
+ ]
75
+
76
+ let listContent
77
+ if (activeTab === 'bookmarks') {
78
+ listContent = <BookmarksList store={window.store} />
79
+ } else {
80
+ listContent = <History store={window.store} />
81
+ }
82
+
62
83
  return (
63
84
  <div
64
85
  ref={menuRef}
@@ -89,11 +110,14 @@ export default function AddBtnMenu ({
89
110
  <RobotOutlined /> {e('createBookmarkByAI')}
90
111
  </div>
91
112
  <QuickConnect batch={batch} inputOnly />
113
+ <Tabs
114
+ activeKey={activeTab}
115
+ onChange={setActiveTab}
116
+ items={tabItems}
117
+ />
92
118
  </div>
93
119
  <div className='add-menu-list'>
94
- <BookmarksList
95
- store={window.store}
96
- />
120
+ {listContent}
97
121
  </div>
98
122
  </div>
99
123
  )
@@ -118,7 +118,7 @@ export default class AddBtn extends Component {
118
118
  focusSearchInput = () => {
119
119
  // Focus the search input after the menu renders
120
120
  this.focusTimeout = setTimeout(() => {
121
- const searchInput = this.menuRef.current?.querySelector('.ant-input')
121
+ const searchInput = this.menuRef.current?.querySelector('.add-menu-list .ant-input')
122
122
  if (searchInput) {
123
123
  searchInput.focus()
124
124
  searchInput.select()
@@ -24,4 +24,12 @@
24
24
  display flex
25
25
  flex-direction column
26
26
  min-height 0
27
+ .sidebar-panel-history
28
+ position relative
29
+ top auto
30
+ left auto
31
+ right auto
32
+ bottom auto
33
+ flex 1
34
+ min-height 0
27
35
 
@@ -82,7 +82,6 @@ class Term extends Component {
82
82
  this.currentInput = ''
83
83
  this.shellInjected = false
84
84
  this.shellType = null
85
- this.manualCommandHistory = new Set()
86
85
  }
87
86
 
88
87
  domRef = createRef()
@@ -737,7 +736,6 @@ class Term extends Component {
737
736
  if (d === '\r' || d === '\n') {
738
737
  const currentCmd = this.getCurrentInput()
739
738
  if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
740
- this.manualCommandHistory.add(currentCmd.trim())
741
739
  window.store.addCmdHistory(currentCmd.trim())
742
740
  }
743
741
  this.closeSuggestions()
@@ -903,9 +901,7 @@ class Term extends Component {
903
901
  }
904
902
 
905
903
  shouldUseManualHistory = () => {
906
- const useManual = this.props.config.showCmdSuggestions &&
907
- (this.shellType === 'sh' || (isWin && this.isLocal()))
908
- return useManual
904
+ return !this.cmdAddon || !this.cmdAddon.hasShellIntegration()
909
905
  }
910
906
 
911
907
  canInjectShellIntegration = () => {
@@ -1078,6 +1074,32 @@ class Term extends Component {
1078
1074
  const { savePassword } = this.state
1079
1075
  const termType = type
1080
1076
  const extra = this.props.sessionOptions
1077
+ // Determine if this is a local terminal (no host)
1078
+ const isLocalType = !tab.host
1079
+ // Build exec settings: only for local type, prefer tab settings over config
1080
+ let execOpts = {}
1081
+ let execPropName = 'execLinux'
1082
+ if (isWin) {
1083
+ execPropName = 'execWindows'
1084
+ } else if (isMac) {
1085
+ execPropName = 'execMac'
1086
+ }
1087
+ if (isLocalType) {
1088
+ // Check flat properties on tab first (bookmark data), then fall back to config
1089
+ if (tab[execPropName]) {
1090
+ // Use bookmark's exec setting directly
1091
+ execOpts = {
1092
+ [execPropName]: tab[execPropName],
1093
+ [`${execPropName}Args`]: tab[`${execPropName}Args`] || []
1094
+ }
1095
+ } else if (config[execPropName]) {
1096
+ // Use global config exec settings
1097
+ execOpts = {
1098
+ [execPropName]: config[execPropName],
1099
+ [`${execPropName}Args`]: config[`${execPropName}Args`] || []
1100
+ }
1101
+ }
1102
+ }
1081
1103
  const opts = clone({
1082
1104
  cols,
1083
1105
  rows,
@@ -1085,18 +1107,13 @@ class Term extends Component {
1085
1107
  saveTerminalLogToFile: config.saveTerminalLogToFile,
1086
1108
  ...tab,
1087
1109
  ...extra,
1110
+ ...execOpts,
1088
1111
  logName,
1089
1112
  sessionLogPath: config.sessionLogPath || createDefaultLogPath(),
1090
1113
  ...pick(config, [
1091
1114
  'addTimeStampToTermLog',
1092
1115
  'keepaliveInterval',
1093
1116
  'keepaliveCountMax',
1094
- 'execWindows',
1095
- 'execMac',
1096
- 'execLinux',
1097
- 'execWindowsArgs',
1098
- 'execMacArgs',
1099
- 'execLinuxArgs',
1100
1117
  'keyword2FA',
1101
1118
  'debug'
1102
1119
  ]),