@electerm/electerm-react 3.15.187 → 3.15.198
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.
- package/client/common/ai-feature.js +10 -0
- package/client/common/setting-list.js +28 -21
- package/client/components/bookmark-form/config/rdp.js +1 -1
- package/client/components/bookmark-form/index.jsx +5 -1
- package/client/components/footer/footer-entry.jsx +2 -1
- package/client/components/main/main.jsx +3 -2
- package/client/components/tabs/add-btn-menu.jsx +9 -6
- package/client/components/tabs/no-session.jsx +9 -6
- package/client/components/terminal/terminal-apis.js +9 -0
- package/client/components/terminal/terminal-command-dropdown.jsx +4 -0
- package/client/components/terminal/terminal.jsx +11 -6
- package/client/components/theme/theme-ai-editor.jsx +233 -0
- package/client/components/theme/theme-form.jsx +93 -40
- package/client/store/mcp-handler.js +203 -1
- package/client/store/system-menu.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the AI feature is disabled globally.
|
|
3
|
+
*
|
|
4
|
+
* Set `window.et.disableAIFeature = true` (the same way `disableUpgradeCheck`
|
|
5
|
+
* is injected into the built index.html `_global` data) to hide every
|
|
6
|
+
* AI-related button / icon / setting menu in the UI.
|
|
7
|
+
*/
|
|
8
|
+
export const isAIDisabled = () => {
|
|
9
|
+
return !!(window.et && window.et.disableAIFeature)
|
|
10
|
+
}
|
|
@@ -5,28 +5,35 @@ import {
|
|
|
5
5
|
settingAiId,
|
|
6
6
|
settingPasswordsId
|
|
7
7
|
} from '../common/constants'
|
|
8
|
+
import { isAIDisabled } from './ai-feature'
|
|
8
9
|
|
|
9
10
|
const e = window.translate
|
|
10
11
|
|
|
11
|
-
export default () =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
12
|
+
export default () => {
|
|
13
|
+
const list = [
|
|
14
|
+
{
|
|
15
|
+
id: settingTerminalId,
|
|
16
|
+
title: e('terminal')
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: settingShortcutsId,
|
|
20
|
+
title: e('settingShortcuts')
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: settingSyncId,
|
|
24
|
+
title: e('settingSync')
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: settingAiId,
|
|
28
|
+
title: 'AI'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: settingPasswordsId,
|
|
32
|
+
title: e('password')
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
if (isAIDisabled()) {
|
|
36
|
+
return list.filter(item => item.id !== settingAiId)
|
|
31
37
|
}
|
|
32
|
-
|
|
38
|
+
return list
|
|
39
|
+
}
|
|
@@ -33,7 +33,7 @@ const rdpConfig = {
|
|
|
33
33
|
commonFields.port,
|
|
34
34
|
{ type: 'profileItem', name: '__profile__', label: '', profileFilter: d => !isEmpty(d.rdp) },
|
|
35
35
|
{ ...commonFields.username, rules: [{ required: true, message: e('username') + ' required' }] },
|
|
36
|
-
|
|
36
|
+
commonFields.password,
|
|
37
37
|
commonFields.description,
|
|
38
38
|
{ type: 'input', name: 'domain', label: () => e('domain') },
|
|
39
39
|
commonFields.proxy,
|
|
@@ -21,6 +21,7 @@ import { LoadingOutlined, BookOutlined, RobotOutlined } from '@ant-design/icons'
|
|
|
21
21
|
import sessionConfig from './config/session-config'
|
|
22
22
|
import renderForm from './render-form'
|
|
23
23
|
import AIBookmarkForm from './ai-bookmark-form'
|
|
24
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
24
25
|
import './bookmark-form.styl'
|
|
25
26
|
|
|
26
27
|
const e = window.translate
|
|
@@ -61,6 +62,9 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
getInitAiModeState () {
|
|
65
|
+
if (isAIDisabled()) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
64
68
|
const v = window.et.openBookmarkWithAIMode
|
|
65
69
|
if (v !== true) {
|
|
66
70
|
return false
|
|
@@ -120,7 +124,7 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
renderAIButton (isNew) {
|
|
123
|
-
if (!isNew || this.state.aiMode) {
|
|
127
|
+
if (!isNew || this.state.aiMode || isAIDisabled()) {
|
|
124
128
|
return null
|
|
125
129
|
}
|
|
126
130
|
return (
|
|
@@ -11,6 +11,7 @@ import encodes from '../bookmark-form/common/encodes'
|
|
|
11
11
|
import { refs } from '../common/ref'
|
|
12
12
|
import Qm from '../quick-commands/quick-commands-select'
|
|
13
13
|
import AIIcon from '../icons/ai-icon'
|
|
14
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
14
15
|
import CmdHistory from './cmd-history'
|
|
15
16
|
|
|
16
17
|
const {
|
|
@@ -189,7 +190,7 @@ export default auto(function FooterEntry (props) {
|
|
|
189
190
|
return (
|
|
190
191
|
<div {...sideProps}>
|
|
191
192
|
<div className='terminal-footer-flex'>
|
|
192
|
-
{renderAIIcon()}
|
|
193
|
+
{!isAIDisabled() && renderAIIcon()}
|
|
193
194
|
{renderCmdHistory()}
|
|
194
195
|
{renderQuickCommands()}
|
|
195
196
|
{renderBatchInputs()}
|
|
@@ -20,6 +20,7 @@ import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
|
|
|
20
20
|
import classnames from 'classnames'
|
|
21
21
|
import ShortcutControl from '../shortcuts/shortcut-control.jsx'
|
|
22
22
|
import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
|
|
23
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
23
24
|
import { ConfigProvider } from 'antd'
|
|
24
25
|
import { NotificationContainer } from '../common/notification'
|
|
25
26
|
import InfoModal from '../sidebar/info-modal.jsx'
|
|
@@ -290,7 +291,7 @@ export default auto(function Index (props) {
|
|
|
290
291
|
<Resolutions {...resProps} />
|
|
291
292
|
<InfoModal {...infoModalProps} />
|
|
292
293
|
<RightSidePanel {...rightPanelProps}>
|
|
293
|
-
<AIChat {...aiChatProps} />
|
|
294
|
+
{!isAIDisabled() && <AIChat {...aiChatProps} />}
|
|
294
295
|
<TerminalInfo key={store.activeTabId} {...terminalInfoProps} />
|
|
295
296
|
</RightSidePanel>
|
|
296
297
|
<SshConfigLoadNotify {...sshConfigProps} />
|
|
@@ -306,7 +307,7 @@ export default auto(function Index (props) {
|
|
|
306
307
|
<BookmarkFromHistoryModal />
|
|
307
308
|
<NotificationContainer />
|
|
308
309
|
<BatchOpRunner />
|
|
309
|
-
<AIConfigModal store={store} />
|
|
310
|
+
{!isAIDisabled() && <AIConfigModal store={store} />}
|
|
310
311
|
<UnixTimestampTooltip />
|
|
311
312
|
</div>
|
|
312
313
|
</ConfigProvider>
|
|
@@ -13,6 +13,7 @@ import BookmarksList from '../sidebar/bookmark-select'
|
|
|
13
13
|
import History from '../sidebar/history'
|
|
14
14
|
import DragHandle from '../common/drag-handle'
|
|
15
15
|
import QuickConnect from './quick-connect'
|
|
16
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
16
17
|
|
|
17
18
|
const e = window.translate
|
|
18
19
|
|
|
@@ -103,12 +104,14 @@ export default function AddBtnMenu ({
|
|
|
103
104
|
<CodeFilled /> {e('newBookmark')}
|
|
104
105
|
</div>
|
|
105
106
|
{addTabBtn}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
{!isAIDisabled() && (
|
|
108
|
+
<div
|
|
109
|
+
className={cls}
|
|
110
|
+
onClick={onNewSshAI}
|
|
111
|
+
>
|
|
112
|
+
<RobotOutlined /> {e('createBookmarkByAI')}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
112
115
|
<QuickConnect batch={batch} inputOnly />
|
|
113
116
|
<Tabs
|
|
114
117
|
activeKey={activeTab}
|
|
@@ -3,6 +3,7 @@ import { RobotOutlined } from '@ant-design/icons'
|
|
|
3
3
|
import LogoElem from '../common/logo-elem.jsx'
|
|
4
4
|
import HistoryPanel from '../sidebar/history'
|
|
5
5
|
import QuickConnect from './quick-connect'
|
|
6
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
6
7
|
import './no-session.styl'
|
|
7
8
|
|
|
8
9
|
const e = window.translate
|
|
@@ -40,12 +41,14 @@ export default function NoSessionPanel ({ height, onNewTab, onNewSsh, batch }) {
|
|
|
40
41
|
>
|
|
41
42
|
{e('newBookmark')}
|
|
42
43
|
</Button>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
{!isAIDisabled() && (
|
|
45
|
+
<Button
|
|
46
|
+
onClick={handleCreateAIBookmark}
|
|
47
|
+
icon={<RobotOutlined />}
|
|
48
|
+
>
|
|
49
|
+
{e('createBookmarkByAI')}
|
|
50
|
+
</Button>
|
|
51
|
+
)}
|
|
49
52
|
<QuickConnect batch={batch} />
|
|
50
53
|
</div>
|
|
51
54
|
<div className='pd3'>
|
|
@@ -19,6 +19,15 @@ export function runCmd (pid, cmd) {
|
|
|
19
19
|
})
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function execCmd (pid, cmd, timeoutMs) {
|
|
23
|
+
return fetch({
|
|
24
|
+
pid,
|
|
25
|
+
cmd,
|
|
26
|
+
timeoutMs,
|
|
27
|
+
action: 'exec-cmd'
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export function resizeTerm (pid, cols, rows) {
|
|
23
32
|
return fetch({
|
|
24
33
|
pid,
|
|
@@ -3,6 +3,7 @@ import { refsStatic, refs } from '../common/ref'
|
|
|
3
3
|
import SuggestionItem from './cmd-item'
|
|
4
4
|
import { aiSuggestionsCache } from '../../common/cache'
|
|
5
5
|
import { appendMandatoryGuardrails } from '../ai/ai-guardrails'
|
|
6
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
6
7
|
import classnames from 'classnames'
|
|
7
8
|
import {
|
|
8
9
|
LoadingOutlined
|
|
@@ -354,6 +355,9 @@ export default class TerminalCmdSuggestions extends Component {
|
|
|
354
355
|
const {
|
|
355
356
|
reverse
|
|
356
357
|
} = this.state
|
|
358
|
+
if (isAIDisabled()) {
|
|
359
|
+
return null
|
|
360
|
+
}
|
|
357
361
|
if (
|
|
358
362
|
(pos === 'top' && !reverse) ||
|
|
359
363
|
(pos === 'bottom' && reverse)
|
|
@@ -42,6 +42,7 @@ import { getFolderFromFilePath } from '../sftp/file-read.js'
|
|
|
42
42
|
import { CommandTrackerAddon } from './command-tracker-addon.js'
|
|
43
43
|
import { Osc52Addon } from './osc52-addon.js'
|
|
44
44
|
import AIIcon from '../icons/ai-icon.jsx'
|
|
45
|
+
import { isAIDisabled } from '../../common/ai-feature.js'
|
|
45
46
|
import {
|
|
46
47
|
AimOutlined
|
|
47
48
|
} from '@ant-design/icons'
|
|
@@ -1043,12 +1044,16 @@ class Term extends Component {
|
|
|
1043
1044
|
label: e('selectall'),
|
|
1044
1045
|
extra: selectAllShortcut
|
|
1045
1046
|
},
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1047
|
+
...(
|
|
1048
|
+
isAIDisabled()
|
|
1049
|
+
? []
|
|
1050
|
+
: [{
|
|
1051
|
+
key: 'explainWithAi',
|
|
1052
|
+
icon: <AIIcon />,
|
|
1053
|
+
label: e('explainWithAi'),
|
|
1054
|
+
disabled: !hasSelection
|
|
1055
|
+
}]
|
|
1056
|
+
),
|
|
1052
1057
|
{
|
|
1053
1058
|
key: 'onClear',
|
|
1054
1059
|
icon: <iconsMap.ReloadOutlined />,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered terminal theme generator.
|
|
3
|
+
* Lets the user describe a theme in natural language, asks the configured AI
|
|
4
|
+
* provider for a full color palette, and writes the result back into the
|
|
5
|
+
* theme form as editable theme text.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect } from 'react'
|
|
8
|
+
import { Button, Input, Space, Alert } from 'antd'
|
|
9
|
+
import {
|
|
10
|
+
RobotOutlined,
|
|
11
|
+
LoadingOutlined
|
|
12
|
+
} from '@ant-design/icons'
|
|
13
|
+
import message from '../common/message'
|
|
14
|
+
import { appendMandatoryGuardrails } from '../ai/ai-guardrails'
|
|
15
|
+
import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
|
|
16
|
+
import HelpIcon from '../common/help-icon'
|
|
17
|
+
import { getItem, setItem } from '../../common/safe-local-storage'
|
|
18
|
+
import {
|
|
19
|
+
requiredThemeProps,
|
|
20
|
+
validThemeProps,
|
|
21
|
+
convertTheme,
|
|
22
|
+
convertThemeToText
|
|
23
|
+
} from '../../common/terminal-theme'
|
|
24
|
+
|
|
25
|
+
const STORAGE_KEY_DESC = 'ai_theme_description'
|
|
26
|
+
const STORAGE_KEY_HISTORY = 'ai_theme_history'
|
|
27
|
+
const EVENT_NAME_HISTORY = 'ai-theme-history-update'
|
|
28
|
+
const { TextArea } = Input
|
|
29
|
+
const e = window.translate
|
|
30
|
+
|
|
31
|
+
// Human readable description for every required theme prop, so the model knows
|
|
32
|
+
// exactly what each key controls and which format to use.
|
|
33
|
+
const themePropDescriptions = {
|
|
34
|
+
main: 'main UI background color (hex #rrggbb)',
|
|
35
|
+
'main-dark': 'darker variant of the main color (hex)',
|
|
36
|
+
'main-light': 'lighter variant of the main color (hex)',
|
|
37
|
+
text: 'main UI text color (hex)',
|
|
38
|
+
'text-light': 'lighter variant of the text color (hex)',
|
|
39
|
+
'text-dark': 'darker variant of the text color (hex)',
|
|
40
|
+
'text-disabled': 'disabled text color (hex)',
|
|
41
|
+
primary: 'primary accent color for buttons/links (hex)',
|
|
42
|
+
info: 'info status color (hex)',
|
|
43
|
+
success: 'success status color (hex)',
|
|
44
|
+
error: 'error status color (hex)',
|
|
45
|
+
warn: 'warning status color (hex)',
|
|
46
|
+
'terminal:foreground': 'default terminal text color (hex or rgba)',
|
|
47
|
+
'terminal:background': 'terminal background color, should relate to main (hex or rgba)',
|
|
48
|
+
'terminal:cursor': 'terminal cursor color (hex or rgba)',
|
|
49
|
+
'terminal:cursorAccent': 'terminal cursor accent color (hex or rgba)',
|
|
50
|
+
'terminal:selectionBackground': 'terminal text selection background (rgba recommended)',
|
|
51
|
+
'terminal:black': 'ANSI color 0 - black (hex)',
|
|
52
|
+
'terminal:red': 'ANSI color 1 - red (hex)',
|
|
53
|
+
'terminal:green': 'ANSI color 2 - green (hex)',
|
|
54
|
+
'terminal:yellow': 'ANSI color 3 - yellow (hex)',
|
|
55
|
+
'terminal:blue': 'ANSI color 4 - blue (hex)',
|
|
56
|
+
'terminal:magenta': 'ANSI color 5 - magenta (hex)',
|
|
57
|
+
'terminal:cyan': 'ANSI color 6 - cyan (hex)',
|
|
58
|
+
'terminal:white': 'ANSI color 7 - white (hex)',
|
|
59
|
+
'terminal:brightBlack': 'ANSI bright color 8 - bright black (hex)',
|
|
60
|
+
'terminal:brightRed': 'ANSI bright color 9 - bright red (hex)',
|
|
61
|
+
'terminal:brightGreen': 'ANSI bright color 10 - bright green (hex)',
|
|
62
|
+
'terminal:brightYellow': 'ANSI bright color 11 - bright yellow (hex)',
|
|
63
|
+
'terminal:brightBlue': 'ANSI bright color 12 - bright blue (hex)',
|
|
64
|
+
'terminal:brightMagenta': 'ANSI bright color 13 - bright magenta (hex)',
|
|
65
|
+
'terminal:brightCyan': 'ANSI bright color 14 - bright cyan (hex)',
|
|
66
|
+
'terminal:brightWhite': 'ANSI bright color 15 - bright white (hex)'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* build the prompt that asks the AI for a complete theme
|
|
71
|
+
* @param {string} description
|
|
72
|
+
* @return {string}
|
|
73
|
+
*/
|
|
74
|
+
function buildThemePrompt (description) {
|
|
75
|
+
const lang = window.store.config.languageAI || window.store.getLangName()
|
|
76
|
+
const fieldList = requiredThemeProps
|
|
77
|
+
.map(key => ` ${key}=${themePropDescriptions[key] || 'color value'}`)
|
|
78
|
+
.join('\n')
|
|
79
|
+
return `You are an electerm terminal theme color generator. Based on the user's natural language description, generate a complete and harmonious color palette by filling in EVERY color listed below.
|
|
80
|
+
|
|
81
|
+
The theme uses a key=value format (one per line). UI colors use hex (#rrggbb). Terminal colors use hex or rgba. The palette should be cohesive, readable and accessible (keep good contrast between foreground and background).
|
|
82
|
+
|
|
83
|
+
Required keys (you MUST fill ALL of them):
|
|
84
|
+
${fieldList}
|
|
85
|
+
|
|
86
|
+
Important rules:
|
|
87
|
+
1. Output ONLY the key=value lines, no markdown code fences, no comments, no extra text
|
|
88
|
+
2. Use exactly the key names listed above
|
|
89
|
+
3. terminal:background should match or relate to main
|
|
90
|
+
4. terminal:foreground must contrast well with terminal:background
|
|
91
|
+
5. Provide distinct, harmonious ANSI 16 colors for the terminal palette
|
|
92
|
+
6. If you include any description text, reply in ${lang} language (but prefer no extra text at all)
|
|
93
|
+
|
|
94
|
+
User description: ${description}
|
|
95
|
+
|
|
96
|
+
Generate the theme:`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Normalize whatever the model returned into clean, valid theme text.
|
|
101
|
+
* Strips markdown fences and drops any key that is not a supported theme prop.
|
|
102
|
+
* @param {string|object} raw
|
|
103
|
+
* @return {string}
|
|
104
|
+
*/
|
|
105
|
+
function normalizeGeneratedTheme (raw) {
|
|
106
|
+
if (!raw) {
|
|
107
|
+
return ''
|
|
108
|
+
}
|
|
109
|
+
let str = typeof raw === 'string' ? raw : JSON.stringify(raw)
|
|
110
|
+
str = str.trim()
|
|
111
|
+
if (str.startsWith('```theme')) {
|
|
112
|
+
str = str.slice(8)
|
|
113
|
+
} else if (str.startsWith('```')) {
|
|
114
|
+
str = str.slice(3)
|
|
115
|
+
}
|
|
116
|
+
if (str.endsWith('```')) {
|
|
117
|
+
str = str.slice(0, -3)
|
|
118
|
+
}
|
|
119
|
+
str = str.trim()
|
|
120
|
+
const converted = convertTheme(str)
|
|
121
|
+
const uiThemeConfig = {}
|
|
122
|
+
const themeConfig = {}
|
|
123
|
+
for (const [key, value] of Object.entries(converted.uiThemeConfig)) {
|
|
124
|
+
if (validThemeProps.includes(key)) {
|
|
125
|
+
uiThemeConfig[key] = value
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const [key, value] of Object.entries(converted.themeConfig)) {
|
|
129
|
+
if (validThemeProps.includes('terminal:' + key)) {
|
|
130
|
+
themeConfig[key] = value
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return convertThemeToText({ themeConfig, uiThemeConfig })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default function ThemeAiEditor (props) {
|
|
137
|
+
const { onChange, disabled } = props
|
|
138
|
+
const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
|
|
139
|
+
const [loading, setLoading] = useState(false)
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setItem(STORAGE_KEY_DESC, description)
|
|
143
|
+
}, [description])
|
|
144
|
+
|
|
145
|
+
async function handleGenerate () {
|
|
146
|
+
if (disabled) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (window.store.aiConfigMissing()) {
|
|
150
|
+
window.store.toggleAIConfig()
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
if (!description.trim()) {
|
|
154
|
+
return message.warning(e('description') + ' required')
|
|
155
|
+
}
|
|
156
|
+
setLoading(true)
|
|
157
|
+
try {
|
|
158
|
+
const config = window.store.config
|
|
159
|
+
const prompt = buildThemePrompt(description)
|
|
160
|
+
const aiResponse = await window.pre.runGlobalAsync(
|
|
161
|
+
'AIchat',
|
|
162
|
+
prompt,
|
|
163
|
+
config.modelAI,
|
|
164
|
+
appendMandatoryGuardrails('You are a helpful assistant that generates electerm terminal theme color palettes.'),
|
|
165
|
+
config.baseURLAI,
|
|
166
|
+
config.apiPathAI,
|
|
167
|
+
config.apiKeyAI,
|
|
168
|
+
config.proxyAI,
|
|
169
|
+
false, // disable streaming for a structured response
|
|
170
|
+
config.authHeaderNameAI
|
|
171
|
+
)
|
|
172
|
+
if (aiResponse && aiResponse.error) {
|
|
173
|
+
throw new Error(aiResponse.error)
|
|
174
|
+
}
|
|
175
|
+
const text = normalizeGeneratedTheme(aiResponse && aiResponse.response)
|
|
176
|
+
if (!text || !text.trim()) {
|
|
177
|
+
throw new Error('empty response')
|
|
178
|
+
}
|
|
179
|
+
onChange(text)
|
|
180
|
+
addHistoryItem(STORAGE_KEY_HISTORY, description, EVENT_NAME_HISTORY)
|
|
181
|
+
message.success(e('Done'))
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('AI theme generation error:', err)
|
|
184
|
+
message.error('Can not generate theme from AI response: ' + err.message)
|
|
185
|
+
} finally {
|
|
186
|
+
setLoading(false)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const textAreaProps = {
|
|
191
|
+
value: description,
|
|
192
|
+
onChange: ev => setDescription(ev.target.value),
|
|
193
|
+
placeholder: e('createThemeByAI'),
|
|
194
|
+
autoSize: { minRows: 4, maxRows: 8 },
|
|
195
|
+
disabled: loading || disabled
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const generateBtnProps = {
|
|
199
|
+
type: 'primary',
|
|
200
|
+
onClick: handleGenerate,
|
|
201
|
+
disabled: !description.trim() || disabled,
|
|
202
|
+
icon: loading ? <LoadingOutlined /> : <RobotOutlined />,
|
|
203
|
+
loading
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className='theme-ai-editor'>
|
|
208
|
+
<div className='pd1b'>
|
|
209
|
+
<Alert
|
|
210
|
+
type='info'
|
|
211
|
+
showIcon
|
|
212
|
+
title={e('aiSecurityNotice')}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
<div className='pd1b'>
|
|
216
|
+
<TextArea {...textAreaProps} />
|
|
217
|
+
</div>
|
|
218
|
+
<AiHistory
|
|
219
|
+
storageKey={STORAGE_KEY_HISTORY}
|
|
220
|
+
eventName={EVENT_NAME_HISTORY}
|
|
221
|
+
onSelect={setDescription}
|
|
222
|
+
/>
|
|
223
|
+
<div className='pd1t'>
|
|
224
|
+
<Space>
|
|
225
|
+
<Button {...generateBtnProps}>
|
|
226
|
+
{e('generate')}
|
|
227
|
+
</Button>
|
|
228
|
+
<HelpIcon link='https://github.com/electerm/electerm/wiki/Create-theme-by-AI' />
|
|
229
|
+
</Space>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { useRef, useState } from 'react'
|
|
2
|
-
import { Button, Input, Form,
|
|
2
|
+
import { Button, Input, Form, Tabs, Flex } from 'antd'
|
|
3
|
+
import {
|
|
4
|
+
BgColorsOutlined,
|
|
5
|
+
FontSizeOutlined,
|
|
6
|
+
RobotOutlined,
|
|
7
|
+
SkinOutlined
|
|
8
|
+
} from '@ant-design/icons'
|
|
3
9
|
import message from '../common/message'
|
|
4
10
|
import {
|
|
5
11
|
convertTheme,
|
|
@@ -10,9 +16,11 @@ import {
|
|
|
10
16
|
} from '../../common/terminal-theme'
|
|
11
17
|
import { defaultTheme, defaultThemeLight } from '../../common/theme-defaults'
|
|
12
18
|
import generate from '../../common/uid'
|
|
19
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
13
20
|
import Link from '../common/external-link'
|
|
14
21
|
import InputAutoFocus from '../common/input-auto-focus'
|
|
15
22
|
import ThemePicker from './theme-editor'
|
|
23
|
+
import ThemeAiEditor from './theme-ai-editor'
|
|
16
24
|
import Upload from '../common/upload'
|
|
17
25
|
// import './theme-form.styl'
|
|
18
26
|
|
|
@@ -48,8 +56,8 @@ export default function ThemeForm (props) {
|
|
|
48
56
|
// A regex to test the hex color format
|
|
49
57
|
const hexColorRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
|
|
50
58
|
|
|
51
|
-
// A regex to test the rgba color format
|
|
52
|
-
const rgbaColorRegex = /^rgba\(\d{1,3}
|
|
59
|
+
// A regex to test the rgba color format (whitespace after commas is optional)
|
|
60
|
+
const rgbaColorRegex = /^rgba\(\d{1,3},\s*\d{1,3},\s*\d{1,3},\s*(0|0?\.\d+|1)\)$/
|
|
53
61
|
|
|
54
62
|
// A message to store the error message
|
|
55
63
|
let message = ''
|
|
@@ -168,9 +176,13 @@ export default function ThemeForm (props) {
|
|
|
168
176
|
setTxt(tt)
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
function handleAiGenerated (text) {
|
|
180
|
+
form.setFieldsValue({
|
|
181
|
+
themeText: text
|
|
182
|
+
})
|
|
183
|
+
setTxt(text)
|
|
184
|
+
// jump to the text editor so the user can review/adjust the result
|
|
185
|
+
setEditor('theme-editor-txt')
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
function renderFuncs (id) {
|
|
@@ -238,12 +250,48 @@ export default function ThemeForm (props) {
|
|
|
238
250
|
}
|
|
239
251
|
const isDefaultTheme = id === defaultTheme().id || id === defaultThemeLight().id
|
|
240
252
|
const disabled = readonly || isDefaultTheme
|
|
241
|
-
const switchTxt = editor === 'theme-editor-txt' ? e('editWithColorPicker') : e('editWithTextEditor')
|
|
242
253
|
const pickerProps = {
|
|
243
254
|
onChange: onPickerChange,
|
|
244
255
|
themeText: txt,
|
|
245
256
|
disabled
|
|
246
257
|
}
|
|
258
|
+
const tabItems = [
|
|
259
|
+
{
|
|
260
|
+
key: 'theme-editor-color-picker',
|
|
261
|
+
label: (
|
|
262
|
+
<span>
|
|
263
|
+
<BgColorsOutlined className='mg1r' />
|
|
264
|
+
{e('editWithColorPicker')}
|
|
265
|
+
</span>
|
|
266
|
+
)
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
key: 'theme-editor-txt',
|
|
270
|
+
label: (
|
|
271
|
+
<span>
|
|
272
|
+
<FontSizeOutlined className='mg1r' />
|
|
273
|
+
{e('editWithTextEditor')}
|
|
274
|
+
</span>
|
|
275
|
+
)
|
|
276
|
+
},
|
|
277
|
+
...(!isAIDisabled()
|
|
278
|
+
? [{
|
|
279
|
+
key: 'theme-editor-ai',
|
|
280
|
+
label: (
|
|
281
|
+
<span>
|
|
282
|
+
<RobotOutlined className='mg1r' />
|
|
283
|
+
AI
|
|
284
|
+
</span>
|
|
285
|
+
)
|
|
286
|
+
}]
|
|
287
|
+
: [])
|
|
288
|
+
]
|
|
289
|
+
const tabsProps = {
|
|
290
|
+
activeKey: editor,
|
|
291
|
+
onChange: setEditor,
|
|
292
|
+
size: 'small',
|
|
293
|
+
items: tabItems
|
|
294
|
+
}
|
|
247
295
|
return (
|
|
248
296
|
<Form
|
|
249
297
|
onFinish={handleSubmit}
|
|
@@ -253,6 +301,11 @@ export default function ThemeForm (props) {
|
|
|
253
301
|
name='terminal-theme-form'
|
|
254
302
|
layout='vertical'
|
|
255
303
|
>
|
|
304
|
+
<div className='mg1b alignright'>
|
|
305
|
+
<Link to='https://theme.electerm.org'>
|
|
306
|
+
<SkinOutlined className='mg1r' />https://theme.electerm.org
|
|
307
|
+
</Link>
|
|
308
|
+
</div>
|
|
256
309
|
{renderFuncs(id)}
|
|
257
310
|
<FormItem
|
|
258
311
|
label={e('themeName')}
|
|
@@ -269,43 +322,43 @@ export default function ThemeForm (props) {
|
|
|
269
322
|
disabled={disabled}
|
|
270
323
|
/>
|
|
271
324
|
</FormItem>
|
|
272
|
-
<FormItem
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
beforeUpload={beforeUpload}
|
|
289
|
-
fileList={[]}
|
|
290
|
-
className='mg1b'
|
|
325
|
+
<FormItem>
|
|
326
|
+
<Flex
|
|
327
|
+
className='mg1b'
|
|
328
|
+
justify='space-between'
|
|
329
|
+
align='center'
|
|
330
|
+
wrap='wrap'
|
|
331
|
+
gap={8}
|
|
332
|
+
>
|
|
333
|
+
<span>{e('themeConfig')}</span>
|
|
334
|
+
<Upload
|
|
335
|
+
beforeUpload={beforeUpload}
|
|
336
|
+
fileList={[]}
|
|
337
|
+
>
|
|
338
|
+
<Button
|
|
339
|
+
type='dashed'
|
|
340
|
+
disabled={disabled}
|
|
291
341
|
>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
</Button>
|
|
298
|
-
</Upload>
|
|
299
|
-
</span>
|
|
300
|
-
</div>
|
|
342
|
+
{e('importFromFile')}
|
|
343
|
+
</Button>
|
|
344
|
+
</Upload>
|
|
345
|
+
</Flex>
|
|
346
|
+
<Tabs {...tabsProps} />
|
|
301
347
|
{
|
|
302
348
|
editor === 'theme-editor-txt'
|
|
303
349
|
? renderTxt()
|
|
304
|
-
:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
350
|
+
: editor === 'theme-editor-ai'
|
|
351
|
+
? (
|
|
352
|
+
<ThemeAiEditor
|
|
353
|
+
onChange={handleAiGenerated}
|
|
354
|
+
disabled={disabled}
|
|
355
|
+
/>
|
|
356
|
+
)
|
|
357
|
+
: (
|
|
358
|
+
<ThemePicker
|
|
359
|
+
{...pickerProps}
|
|
360
|
+
/>
|
|
361
|
+
)
|
|
309
362
|
}
|
|
310
363
|
</FormItem>
|
|
311
364
|
{
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import uid from '../common/uid'
|
|
7
7
|
import { settingMap } from '../common/constants'
|
|
8
8
|
import { refs, refsTabs } from '../components/common/ref'
|
|
9
|
-
import { runCmd } from '../components/terminal/terminal-apis'
|
|
9
|
+
import { runCmd, execCmd } from '../components/terminal/terminal-apis'
|
|
10
10
|
import deepCopy from 'json-deep-copy'
|
|
11
11
|
import {
|
|
12
12
|
getLocalFileInfo,
|
|
@@ -125,6 +125,9 @@ export default Store => {
|
|
|
125
125
|
case 'cancel_terminal_command':
|
|
126
126
|
result = store.mcpCancelTerminalCommand(args)
|
|
127
127
|
break
|
|
128
|
+
case 'execute_command':
|
|
129
|
+
result = await store.mcpExecuteCommand(args)
|
|
130
|
+
break
|
|
128
131
|
|
|
129
132
|
// Background task operations
|
|
130
133
|
case 'run_background_command':
|
|
@@ -139,6 +142,9 @@ export default Store => {
|
|
|
139
142
|
case 'cancel_background_task':
|
|
140
143
|
result = await store.mcpCancelBackgroundTask(args)
|
|
141
144
|
break
|
|
145
|
+
case 'cleanup_background_task':
|
|
146
|
+
result = await store.mcpCleanupBackgroundTask(args)
|
|
147
|
+
break
|
|
142
148
|
|
|
143
149
|
// SFTP operations
|
|
144
150
|
case 'sftp_list':
|
|
@@ -738,6 +744,173 @@ export default Store => {
|
|
|
738
744
|
}
|
|
739
745
|
}
|
|
740
746
|
|
|
747
|
+
// ==================== Structured Command Execution ====================
|
|
748
|
+
|
|
749
|
+
// Keep at most maxChars of a string, favoring the tail (most recent output).
|
|
750
|
+
function truncateTail (str, maxChars) {
|
|
751
|
+
if (!str || !maxChars || str.length <= maxChars) {
|
|
752
|
+
return { text: str || '', truncated: false }
|
|
753
|
+
}
|
|
754
|
+
return { text: str.slice(-maxChars), truncated: true }
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Read up to maxLines lines from the active terminal buffer.
|
|
758
|
+
function collectBufferLines (tabId, maxLines = 800) {
|
|
759
|
+
const term = refs.get('term-' + tabId)
|
|
760
|
+
if (!term || !term.term) return []
|
|
761
|
+
const buffer = term.term.buffer.active
|
|
762
|
+
if (!buffer) return []
|
|
763
|
+
const cursorY = buffer.cursorY || 0
|
|
764
|
+
const baseY = buffer.baseY || 0
|
|
765
|
+
const totalLines = buffer.length || 0
|
|
766
|
+
const end = Math.min(totalLines, baseY + cursorY + 1)
|
|
767
|
+
const start = Math.max(0, end - maxLines)
|
|
768
|
+
const lines = []
|
|
769
|
+
for (let i = start; i < end; i++) {
|
|
770
|
+
const line = buffer.getLine(i)
|
|
771
|
+
lines.push(line ? line.translateToString(true) : '')
|
|
772
|
+
}
|
|
773
|
+
return lines
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// PTY fallback for non-SSH tabs: wrap the command with unique begin/end
|
|
777
|
+
// sentinels, then poll the terminal buffer for the end sentinel carrying
|
|
778
|
+
// the exit code. stderr cannot be separated in a PTY — it is merged into
|
|
779
|
+
// stdout and flagged via stderrMerged.
|
|
780
|
+
Store.prototype.mcpExecuteCommandPty = async function (args) {
|
|
781
|
+
const { store } = window
|
|
782
|
+
const { command, tabId, timeoutMs } = args
|
|
783
|
+
const marker = `__ET_EXEC_${uid()}__`
|
|
784
|
+
const startLine = `${marker}S`
|
|
785
|
+
const wrapped = `echo "${startLine}"; ${command}; echo "${marker}E$?"`
|
|
786
|
+
|
|
787
|
+
store.runQuickCommand(wrapped, false, tabId)
|
|
788
|
+
|
|
789
|
+
const start = Date.now()
|
|
790
|
+
const pollInterval = 300
|
|
791
|
+
const endRe = new RegExp(`^${marker}E(\\d+)$`)
|
|
792
|
+
|
|
793
|
+
while (Date.now() - start < timeoutMs) {
|
|
794
|
+
const lines = collectBufferLines(tabId)
|
|
795
|
+
let endIdx = -1
|
|
796
|
+
let exitCode = null
|
|
797
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
798
|
+
const m = lines[i].trim().match(endRe)
|
|
799
|
+
if (m) {
|
|
800
|
+
endIdx = i
|
|
801
|
+
exitCode = parseInt(m[1], 10)
|
|
802
|
+
break
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (endIdx >= 0) {
|
|
806
|
+
let startIdx = -1
|
|
807
|
+
for (let i = endIdx - 1; i >= 0; i--) {
|
|
808
|
+
if (lines[i].trim() === startLine) {
|
|
809
|
+
startIdx = i
|
|
810
|
+
break
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const outLines = lines.slice(startIdx + 1, endIdx)
|
|
814
|
+
return {
|
|
815
|
+
stdout: outLines.join('\n').replace(/\n+$/, ''),
|
|
816
|
+
stderr: '',
|
|
817
|
+
stderrMerged: true,
|
|
818
|
+
exitCode,
|
|
819
|
+
durationMs: Date.now() - start,
|
|
820
|
+
timedOut: false
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Timeout: return whatever followed the start sentinel (if it appeared)
|
|
827
|
+
const lines = collectBufferLines(tabId)
|
|
828
|
+
let startIdx = -1
|
|
829
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
830
|
+
if (lines[i].trim() === startLine) {
|
|
831
|
+
startIdx = i
|
|
832
|
+
break
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const outLines = startIdx >= 0 ? lines.slice(startIdx + 1) : []
|
|
836
|
+
return {
|
|
837
|
+
stdout: outLines.join('\n').replace(/\n+$/, ''),
|
|
838
|
+
stderr: '',
|
|
839
|
+
stderrMerged: true,
|
|
840
|
+
exitCode: null,
|
|
841
|
+
durationMs: Date.now() - start,
|
|
842
|
+
timedOut: true
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
Store.prototype.mcpExecuteCommand = async function (args) {
|
|
847
|
+
const { store } = window
|
|
848
|
+
const { command } = args
|
|
849
|
+
const tabId = args.tabId || store.activeTabId
|
|
850
|
+
const timeoutMs = Math.min(Math.max(args.timeoutMs || 120000, 1000), 600000)
|
|
851
|
+
const maxOutputBytes = args.maxOutputBytes || 204800
|
|
852
|
+
// 'exec' (default): SSH exec channel with sentinel-PTY fallback.
|
|
853
|
+
// 'pty': force sentinel capture in the visible terminal — for commands
|
|
854
|
+
// that need a TTY (color output, sudo prompts, TTY-dependent tools).
|
|
855
|
+
const requestedMode = args.mode === 'pty' ? 'pty' : 'exec'
|
|
856
|
+
|
|
857
|
+
if (!tabId) {
|
|
858
|
+
throw new Error('No active terminal')
|
|
859
|
+
}
|
|
860
|
+
if (!command) {
|
|
861
|
+
throw new Error('No command provided')
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const tab = store.tabs.find(t => t.id === tabId)
|
|
865
|
+
if (!tab) {
|
|
866
|
+
throw new Error(`Tab not found: ${tabId}`)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const isSsh = tab.type === 'ssh' || (!tab.type && !!tab.host)
|
|
870
|
+
const start = Date.now()
|
|
871
|
+
let result = null
|
|
872
|
+
let mode = 'pty'
|
|
873
|
+
|
|
874
|
+
if (requestedMode === 'exec' && isSsh) {
|
|
875
|
+
try {
|
|
876
|
+
const r = await execCmd(tabId, command, timeoutMs)
|
|
877
|
+
result = {
|
|
878
|
+
stdout: r.stdout || '',
|
|
879
|
+
stderr: r.stderr || '',
|
|
880
|
+
stderrMerged: false,
|
|
881
|
+
exitCode: typeof r.exitCode === 'number' ? r.exitCode : null,
|
|
882
|
+
durationMs: Date.now() - start,
|
|
883
|
+
timedOut: !!r.timedOut
|
|
884
|
+
}
|
|
885
|
+
mode = 'exec'
|
|
886
|
+
} catch (e) {
|
|
887
|
+
// Exec channel unavailable (e.g. connection dropped) — fall back to PTY
|
|
888
|
+
if (!/not supported/i.test(e.message || '')) {
|
|
889
|
+
throw e
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (!result) {
|
|
895
|
+
result = await store.mcpExecuteCommandPty({ command, tabId, timeoutMs })
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const out = truncateTail(result.stdout, maxOutputBytes)
|
|
899
|
+
const err = truncateTail(result.stderr, maxOutputBytes)
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
stdout: out.text,
|
|
903
|
+
stderr: err.text,
|
|
904
|
+
stderrMerged: result.stderrMerged,
|
|
905
|
+
exitCode: result.exitCode,
|
|
906
|
+
durationMs: result.durationMs,
|
|
907
|
+
timedOut: result.timedOut,
|
|
908
|
+
truncated: out.truncated || err.truncated,
|
|
909
|
+
mode,
|
|
910
|
+
tabId
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
741
914
|
// ==================== Background Task Management ====================
|
|
742
915
|
|
|
743
916
|
const backgroundTasks = new Map()
|
|
@@ -814,6 +987,18 @@ export default Store => {
|
|
|
814
987
|
const pid = pidOutput.trim()
|
|
815
988
|
|
|
816
989
|
if (!pid) {
|
|
990
|
+
// PID file is gone. The wrapper removes it when the command finishes,
|
|
991
|
+
// so a missing PID file usually means COMPLETED — check the exit file
|
|
992
|
+
// before reporting unknown.
|
|
993
|
+
const exitOutput = await runMonitorCmd(task.tabId,
|
|
994
|
+
`cat ${task.exitFile} 2>/dev/null`)
|
|
995
|
+
const exitCode = exitOutput.trim()
|
|
996
|
+
if (exitCode !== '') {
|
|
997
|
+
task.status = 'completed'
|
|
998
|
+
task.exitCode = parseInt(exitCode, 10)
|
|
999
|
+
task.endTime = Date.now()
|
|
1000
|
+
return { ...task, status: 'completed', exitCode: task.exitCode }
|
|
1001
|
+
}
|
|
817
1002
|
return { ...task, status: 'unknown', message: 'PID file not found' }
|
|
818
1003
|
}
|
|
819
1004
|
|
|
@@ -883,6 +1068,23 @@ export default Store => {
|
|
|
883
1068
|
}
|
|
884
1069
|
}
|
|
885
1070
|
|
|
1071
|
+
// Remove remote temp files (log/pid/exit) and untrack the task.
|
|
1072
|
+
// Used by the MCP Tasks TTL sweep; safe to call on unknown tasks.
|
|
1073
|
+
Store.prototype.mcpCleanupBackgroundTask = async function (args) {
|
|
1074
|
+
const task = backgroundTasks.get(args.taskId)
|
|
1075
|
+
if (!task) {
|
|
1076
|
+
return { success: true, message: 'Task not tracked (already cleaned)' }
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
await runMonitorCmd(task.tabId,
|
|
1080
|
+
`rm -f ${task.logFile} ${task.pidFile} ${task.exitFile}`)
|
|
1081
|
+
} catch (_) {
|
|
1082
|
+
// best-effort remote cleanup
|
|
1083
|
+
}
|
|
1084
|
+
backgroundTasks.delete(args.taskId)
|
|
1085
|
+
return { success: true, taskId: args.taskId }
|
|
1086
|
+
}
|
|
1087
|
+
|
|
886
1088
|
// ==================== Settings APIs ====================
|
|
887
1089
|
|
|
888
1090
|
Store.prototype.mcpGetSettings = function () {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
maxZoom,
|
|
11
11
|
minZoom
|
|
12
12
|
} from '../common/constants'
|
|
13
|
+
import { isAIDisabled } from '../common/ai-feature'
|
|
13
14
|
|
|
14
15
|
const e = window.translate
|
|
15
16
|
|
|
@@ -63,6 +64,9 @@ export default Store => {
|
|
|
63
64
|
|
|
64
65
|
Store.prototype.onNewSshAI = function () {
|
|
65
66
|
const { store } = window
|
|
67
|
+
if (isAIDisabled()) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
66
70
|
if (store.aiConfigMissing()) {
|
|
67
71
|
store.toggleAIConfig()
|
|
68
72
|
return
|