@electerm/electerm-react 5.3.15 → 5.5.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.
- package/README.md +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +23 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +14 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/bookmark-form/index.jsx +10 -8
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +80 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/col.jsx +32 -6
- package/client/components/setting-panel/list.styl +12 -0
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-mobile-breadcrumb.jsx +29 -0
- package/client/components/setting-panel/setting-modal.jsx +64 -44
- package/client/components/setting-panel/setting-terminal.jsx +7 -2
- package/client/components/setting-panel/setting-wrap.jsx +0 -3
- package/client/components/setting-panel/setting-wrap.styl +80 -11
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sidebar/index.jsx +28 -21
- package/client/components/sidebar/left-sidebar-icons.jsx +1 -1
- package/client/components/sidebar/sidebar.styl +16 -0
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/tabs/tabs.styl +4 -0
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +88 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +136 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-entry.jsx +32 -27
- package/client/components/terminal/shortcut-bar.jsx +4 -6
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +56 -0
- package/client/components/terminal/startup-queue.js +379 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal.jsx +102 -2007
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/theme/theme-form.jsx +22 -0
- package/client/components/tree-list/bookmark-transport.jsx +7 -31
- package/client/components/tree-list/tree-list.styl +13 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +9 -1
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +14 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +20 -0
- package/client/store/sidebar.js +12 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +45 -9
- package/client/store/system-menu.js +2 -0
- package/client/store/tab.js +41 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger list for the settings panel (left col), follows quick commands list.
|
|
3
|
+
*/
|
|
4
|
+
import List from '../setting-panel/list'
|
|
5
|
+
import { PlusOutlined, CopyOutlined } from '@ant-design/icons'
|
|
6
|
+
import classnames from 'classnames'
|
|
7
|
+
import highlight from '../common/highlight'
|
|
8
|
+
import deepCopy from 'json-deep-copy'
|
|
9
|
+
import uid from '../../common/uid'
|
|
10
|
+
import TriggerTransport from './trigger-transport.jsx'
|
|
11
|
+
|
|
12
|
+
const e = window.translate
|
|
13
|
+
|
|
14
|
+
export default class TriggerSettingList extends List {
|
|
15
|
+
del = (item, e) => {
|
|
16
|
+
e.stopPropagation()
|
|
17
|
+
this.props.store.delTrigger(item)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onClickItem = (item) => {
|
|
21
|
+
this.props.onClickItem(item)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
duplicateItem = (e, item) => {
|
|
25
|
+
e.stopPropagation()
|
|
26
|
+
const copy = deepCopy(item)
|
|
27
|
+
copy.id = uid()
|
|
28
|
+
copy.name = (item.name || 'Unnamed') + ' (copy)'
|
|
29
|
+
window.store.addTrigger(copy)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
renderDuplicateBtn = (item) => {
|
|
33
|
+
if (!item.id) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
return (
|
|
37
|
+
<CopyOutlined
|
|
38
|
+
title={e('duplicate')}
|
|
39
|
+
className='pointer list-item-duplicate'
|
|
40
|
+
onClick={(e) => this.duplicateItem(e, item)}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
renderItem = (item) => {
|
|
46
|
+
if (!item) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
const { activeItemId } = this.props
|
|
50
|
+
const { name, id } = item
|
|
51
|
+
const cls = classnames(
|
|
52
|
+
'item-list-unit',
|
|
53
|
+
{
|
|
54
|
+
active: activeItemId === id
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
let title = name || 'Unnamed'
|
|
58
|
+
title = highlight(
|
|
59
|
+
title,
|
|
60
|
+
this.state.keyword
|
|
61
|
+
)
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
key={id}
|
|
65
|
+
className={cls}
|
|
66
|
+
onClick={() => this.onClickItem(item)}
|
|
67
|
+
>
|
|
68
|
+
<div className='elli pd1y pd2x' title={name}>
|
|
69
|
+
{
|
|
70
|
+
!id
|
|
71
|
+
? <PlusOutlined className='mg1r' />
|
|
72
|
+
: null
|
|
73
|
+
}
|
|
74
|
+
{title}
|
|
75
|
+
</div>
|
|
76
|
+
{this.renderDuplicateBtn(item)}
|
|
77
|
+
{this.renderDelBtn(item)}
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
renderTransport = () => {
|
|
83
|
+
return (
|
|
84
|
+
<TriggerTransport
|
|
85
|
+
store={this.props.store}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger import/export, follows quick command transport.
|
|
3
|
+
*/
|
|
4
|
+
import BookmarkTransport from '../tree-list/bookmark-transport'
|
|
5
|
+
import download from '../../common/download'
|
|
6
|
+
import time from '../../common/time'
|
|
7
|
+
import { runImportTask } from '../../common/import-task'
|
|
8
|
+
import { normalizeTrigger } from '../../store/trigger'
|
|
9
|
+
import { validateTriggers } from '../terminal/automation/trigger-engine'
|
|
10
|
+
|
|
11
|
+
const e = window.translate
|
|
12
|
+
|
|
13
|
+
export default class TriggerTransport extends BookmarkTransport {
|
|
14
|
+
name = 'triggers'
|
|
15
|
+
|
|
16
|
+
beforeUpload = async (file) => {
|
|
17
|
+
const { store } = this.props
|
|
18
|
+
const txt = file.fileContent !== undefined
|
|
19
|
+
? file.fileContent
|
|
20
|
+
: await window.fs.readFile(file.filePath)
|
|
21
|
+
try {
|
|
22
|
+
const arr = JSON.parse(txt).map(normalizeTrigger)
|
|
23
|
+
const errors = validateTriggers(arr)
|
|
24
|
+
if (errors.length) {
|
|
25
|
+
throw new Error(errors[0])
|
|
26
|
+
}
|
|
27
|
+
const state = store[this.name]
|
|
28
|
+
const existing = new Set(state.map(v => v.id))
|
|
29
|
+
const fresh = arr.filter(t => !existing.has(t.id))
|
|
30
|
+
if (!fresh.length) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
await runImportTask({
|
|
34
|
+
title: e('import'),
|
|
35
|
+
batch: 200,
|
|
36
|
+
stopWatchers: [this.name],
|
|
37
|
+
steps: [
|
|
38
|
+
{
|
|
39
|
+
label: e('triggers'),
|
|
40
|
+
items: fresh,
|
|
41
|
+
process: (chunk) => {
|
|
42
|
+
state.push(...chunk)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
})
|
|
47
|
+
store.refreshAllTriggers()
|
|
48
|
+
} catch (err) {
|
|
49
|
+
store.onError(err)
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
handleDownload = () => {
|
|
55
|
+
const { store } = this.props
|
|
56
|
+
const txt = JSON.stringify(store[this.name] || [], null, 2)
|
|
57
|
+
const stamp = time(undefined, 'YYYY-MM-DD-HH-mm-ss')
|
|
58
|
+
download('electerm-' + this.name + '-' + stamp + '.json', txt)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookmark form wrapper for triggers.
|
|
3
|
+
* Works as an antd Form field (value/onChange injected by Form.Item).
|
|
4
|
+
*/
|
|
5
|
+
import { Form } from 'antd'
|
|
6
|
+
import TriggerEditor from './trigger-editor.jsx'
|
|
7
|
+
|
|
8
|
+
const e = window.translate
|
|
9
|
+
|
|
10
|
+
export default function TriggersField () {
|
|
11
|
+
return (
|
|
12
|
+
<Form.Item
|
|
13
|
+
name='triggers'
|
|
14
|
+
label={e('triggers')}
|
|
15
|
+
>
|
|
16
|
+
<TriggerEditor />
|
|
17
|
+
</Form.Item>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -9,13 +9,16 @@ import {
|
|
|
9
9
|
import WidgetInstances from './widget-instances'
|
|
10
10
|
import classnames from 'classnames'
|
|
11
11
|
import highlight from '../common/highlight'
|
|
12
|
+
import {
|
|
13
|
+
settingPanelMobileBreakpoint
|
|
14
|
+
} from '../../common/constants'
|
|
12
15
|
import {
|
|
13
16
|
auto
|
|
14
17
|
} from 'manate/react'
|
|
15
18
|
|
|
16
19
|
const e = window.translate
|
|
17
20
|
|
|
18
|
-
export default auto(function WidgetsList ({ activeItemId, store }) {
|
|
21
|
+
export default auto(function WidgetsList ({ activeItemId, store, onClickItem }) {
|
|
19
22
|
const { widgetInstances } = store
|
|
20
23
|
const [tab, setTab] = useState('widgets') // or instances
|
|
21
24
|
const [widgets, setWidgets] = useState([])
|
|
@@ -50,7 +53,14 @@ export default auto(function WidgetsList ({ activeItemId, store }) {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const onClickWidget = (widget) => {
|
|
56
|
+
if (onClickItem) {
|
|
57
|
+
return onClickItem(widget)
|
|
58
|
+
}
|
|
53
59
|
window.store.setSettingItem(widget)
|
|
60
|
+
// narrow panels drill into the right col content after picking a widget
|
|
61
|
+
if (window.store.innerWidth <= settingPanelMobileBreakpoint) {
|
|
62
|
+
window.store.settingMobileView = 'content'
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
const renderWidgetItem = (widget, i) => {
|
package/client/css/mobile.styl
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
// Breakpoint: 600px (see mobileBreakpoint in constants.js)
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
.is-mobile
|
|
6
|
+
// Keep every terminal footer row on the same positioning reference as the
|
|
7
|
+
// touch shortcut bar. Mobile browsers change the visual viewport when their
|
|
8
|
+
// chrome or soft keyboard opens; mixing absolute footer rows with the fixed
|
|
9
|
+
// shortcut row makes the monitor/shortcut stack detach from the footer.
|
|
10
|
+
.main-footer,
|
|
11
|
+
.remote-monitor-bar
|
|
12
|
+
position fixed
|
|
13
|
+
|
|
6
14
|
// ---- 1. Tabs: show only current tab + dropdown, close icon always visible
|
|
7
15
|
.tabs.mobile-tabs
|
|
8
16
|
display flex
|
|
@@ -81,7 +89,7 @@
|
|
|
81
89
|
position fixed
|
|
82
90
|
left 0
|
|
83
91
|
right 0
|
|
84
|
-
bottom 36px
|
|
92
|
+
bottom calc(var(--footer-stack-height, 36px) + var(--shortcut-bar-h, 0px) + var(--shortcut-bar-kb-offset, 0px))
|
|
85
93
|
z-index 200
|
|
86
94
|
background var(--main)
|
|
87
95
|
padding 8px 10px
|
package/client/store/common.js
CHANGED
|
@@ -276,6 +276,14 @@ export default Store => {
|
|
|
276
276
|
const { store } = window
|
|
277
277
|
store.rightPanelVisible = true
|
|
278
278
|
store.rightPanelTab = 'ai'
|
|
279
|
+
// Ask for AI config right away when AI is not configured yet.
|
|
280
|
+
// This has to happen here (a plain user action) instead of from
|
|
281
|
+
// AIChat's mount effect: a write issued there lands in the very commit
|
|
282
|
+
// that opened the panel, and that update is dropped, so the config
|
|
283
|
+
// modal never showed up on first open.
|
|
284
|
+
if (store.aiConfigMissing()) {
|
|
285
|
+
store.toggleAIConfig()
|
|
286
|
+
}
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
Store.prototype.explainWithAi = function (txt) {
|
|
@@ -377,7 +385,10 @@ export default Store => {
|
|
|
377
385
|
content: 'Understood. I will use this context as we continue.'
|
|
378
386
|
})
|
|
379
387
|
} else {
|
|
380
|
-
messages.push({
|
|
388
|
+
messages.push({
|
|
389
|
+
role: 'user',
|
|
390
|
+
content: entry.promptWithAttachments || entry.prompt
|
|
391
|
+
})
|
|
381
392
|
if (entry.response) {
|
|
382
393
|
messages.push({ role: 'assistant', content: entry.response })
|
|
383
394
|
}
|
|
@@ -134,6 +134,8 @@ export default () => {
|
|
|
134
134
|
settingTab: settingMap.bookmarks, // setting tab
|
|
135
135
|
bookmarkId: undefined,
|
|
136
136
|
showModal: 0,
|
|
137
|
+
// mobile settings view: 'menu' (left col list, default) or 'content' (right col)
|
|
138
|
+
settingMobileView: 'menu',
|
|
137
139
|
|
|
138
140
|
// setting sync related
|
|
139
141
|
autoSyncReady: false,
|
|
@@ -165,6 +167,10 @@ export default () => {
|
|
|
165
167
|
pinnedQuickCommandBar: ls.getItem(pinnedQuickCommandBarKey) === 'y',
|
|
166
168
|
qmSortByFrequency: ls.getItem(qmSortByFrequencyKey) === 'yes',
|
|
167
169
|
|
|
170
|
+
// declarative auto triggers (global; bookmark/session ones live on the tab)
|
|
171
|
+
triggers: [],
|
|
172
|
+
triggerSessionOpen: false,
|
|
173
|
+
|
|
168
174
|
// touch-only shortcut bar (see components/terminal/shortcut-bar-entry.jsx)
|
|
169
175
|
shortcutBarVisible: false,
|
|
170
176
|
// px the bar is lifted above a page-overlaying system input panel
|
|
@@ -187,6 +193,9 @@ export default () => {
|
|
|
187
193
|
|
|
188
194
|
// editor
|
|
189
195
|
showEditor: false,
|
|
196
|
+
// set to true to mount the text editor component,
|
|
197
|
+
// so its code is only loaded when user really needs it
|
|
198
|
+
textEditorRequested: false,
|
|
190
199
|
|
|
191
200
|
// file/info modal
|
|
192
201
|
showFileModal: false,
|
|
@@ -213,6 +222,11 @@ export default () => {
|
|
|
213
222
|
isMaximized: window.pre.runSync('isMaximized'),
|
|
214
223
|
hasNodePty: window.pre.runSync('nodePtyCheck'),
|
|
215
224
|
isMobile: window.innerWidth <= mobileBreakpoint,
|
|
225
|
+
// reflects the input device actually in use, not screen capability — a
|
|
226
|
+
// capability probe only proves the screen can be touched, not whether the
|
|
227
|
+
// user operates it by touch. Seeded false and left to main.jsx, which sets
|
|
228
|
+
// mouse → false / touch → true on the first real pointer event.
|
|
229
|
+
isTouchDevice: false,
|
|
216
230
|
fullscreen: false,
|
|
217
231
|
tabsHeight: 36,
|
|
218
232
|
|
|
@@ -35,6 +35,36 @@ const dangerousTabProps = [
|
|
|
35
35
|
'interactiveValues'
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
+
// Tab/session UI state keys that must never be set by MCP/AI callers.
|
|
39
|
+
// A crafted `batch` (missing, string, or out-of-range) used to crash the
|
|
40
|
+
// Sessions render (`sizes[batch]` undefined -> cannot destructure height).
|
|
41
|
+
// `addTab` now clamps batch too, but strip here as first line of defense.
|
|
42
|
+
const tabInternalProps = [
|
|
43
|
+
'batch',
|
|
44
|
+
'id',
|
|
45
|
+
'status',
|
|
46
|
+
'pane',
|
|
47
|
+
'tabCount',
|
|
48
|
+
'from',
|
|
49
|
+
'srcId',
|
|
50
|
+
'sftpCreated',
|
|
51
|
+
'isTransporting',
|
|
52
|
+
'mcpStatus',
|
|
53
|
+
'activeTabId',
|
|
54
|
+
'sshSftpSplitView',
|
|
55
|
+
'sshTunnelResults',
|
|
56
|
+
'displayRaw',
|
|
57
|
+
'autoReConnect',
|
|
58
|
+
'_reloadState',
|
|
59
|
+
'isPinned'
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
function stripTabInternalProps (obj) {
|
|
63
|
+
return Object.fromEntries(
|
|
64
|
+
Object.entries(obj).filter(([key]) => !tabInternalProps.includes(key))
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
38
68
|
// Strip dangerous props from an object, returning a safe copy.
|
|
39
69
|
function stripDangerousTabProps (obj) {
|
|
40
70
|
return Object.fromEntries(
|
|
@@ -42,15 +72,64 @@ function stripDangerousTabProps (obj) {
|
|
|
42
72
|
)
|
|
43
73
|
}
|
|
44
74
|
|
|
75
|
+
// ==================== Tab MCP status tag helpers ====================
|
|
76
|
+
// Reflect MCP activity on the tab title via tab.mcpStatus
|
|
77
|
+
// (rendered as [running]/[waiting]/[done]/[error] by create-title.jsx).
|
|
78
|
+
|
|
79
|
+
const mcpStatusTimers = new Map()
|
|
80
|
+
|
|
81
|
+
function findMcpTab (tabId) {
|
|
82
|
+
return window.store.tabs.find(t => t.id === tabId)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setTabMcpStatus (tabId, status) {
|
|
86
|
+
const tab = findMcpTab(tabId)
|
|
87
|
+
if (!tab) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const timer = mcpStatusTimers.get(tabId)
|
|
91
|
+
if (timer) {
|
|
92
|
+
clearTimeout(timer)
|
|
93
|
+
mcpStatusTimers.delete(tabId)
|
|
94
|
+
}
|
|
95
|
+
tab.mcpStatus = status
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Set a status that auto-clears after clearDelay ms (clearDelay 0 = keep
|
|
99
|
+
// until the next status change). The clear only applies if the status
|
|
100
|
+
// has not been replaced in the meantime.
|
|
101
|
+
function setTabMcpStatusAuto (tabId, status, clearDelay = 6000) {
|
|
102
|
+
setTabMcpStatus(tabId, status)
|
|
103
|
+
if (!clearDelay) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
mcpStatusTimers.set(tabId, setTimeout(() => {
|
|
107
|
+
mcpStatusTimers.delete(tabId)
|
|
108
|
+
const tab = findMcpTab(tabId)
|
|
109
|
+
if (tab && tab.mcpStatus === status) {
|
|
110
|
+
tab.mcpStatus = ''
|
|
111
|
+
}
|
|
112
|
+
}, clearDelay))
|
|
113
|
+
}
|
|
114
|
+
|
|
45
115
|
export default Store => {
|
|
46
116
|
// Initialize MCP handler - called when MCP widget is started
|
|
47
117
|
Store.prototype.initMcpHandler = function () {
|
|
118
|
+
if (window._mcpHandlerInitialized) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
window._mcpHandlerInitialized = true
|
|
48
122
|
const { ipcOnEvent } = window.pre
|
|
49
123
|
// Listen for MCP requests from main process
|
|
50
124
|
ipcOnEvent('mcp-request', (event, request) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
125
|
+
try {
|
|
126
|
+
const { requestId, action, data } = request || {}
|
|
127
|
+
if (action === 'tool-call') {
|
|
128
|
+
window.store.handleMcpToolCall(requestId, data && data.toolName, data && data.args)
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Never let a malformed IPC payload break the UI thread
|
|
132
|
+
console.error('mcp-request dispatch error', err)
|
|
54
133
|
}
|
|
55
134
|
})
|
|
56
135
|
}
|
|
@@ -58,6 +137,7 @@ export default Store => {
|
|
|
58
137
|
// Handle individual tool calls
|
|
59
138
|
Store.prototype.handleMcpToolCall = async function (requestId, toolName, args) {
|
|
60
139
|
const { store } = window
|
|
140
|
+
args = args || {}
|
|
61
141
|
|
|
62
142
|
try {
|
|
63
143
|
let result
|
|
@@ -266,9 +346,10 @@ export default Store => {
|
|
|
266
346
|
|
|
267
347
|
Store.prototype.mcpAddBookmark = async function (args) {
|
|
268
348
|
const { store } = window
|
|
349
|
+
const safeArgs = stripTabInternalProps(stripDangerousTabProps({ ...(args || {}) }))
|
|
269
350
|
const bookmark = fixBookmarkData({
|
|
270
|
-
|
|
271
|
-
|
|
351
|
+
...safeArgs,
|
|
352
|
+
id: uid()
|
|
272
353
|
})
|
|
273
354
|
|
|
274
355
|
const { valid, errors } = validateBookmarkData(bookmark)
|
|
@@ -287,14 +368,16 @@ export default Store => {
|
|
|
287
368
|
|
|
288
369
|
Store.prototype.mcpEditBookmark = function (args) {
|
|
289
370
|
const { store } = window
|
|
290
|
-
const { id, updates } = args
|
|
371
|
+
const { id, updates } = args || {}
|
|
291
372
|
|
|
292
373
|
const bookmark = store.bookmarks.find(b => b.id === id)
|
|
293
374
|
if (!bookmark) {
|
|
294
375
|
throw new Error(`Bookmark not found: ${id}`)
|
|
295
376
|
}
|
|
296
377
|
|
|
297
|
-
|
|
378
|
+
const safeUpdates = stripTabInternalProps(stripDangerousTabProps({ ...(updates || {}) }))
|
|
379
|
+
delete safeUpdates.id
|
|
380
|
+
store.editItem(id, safeUpdates, settingMap.bookmarks)
|
|
298
381
|
|
|
299
382
|
return {
|
|
300
383
|
success: true,
|
|
@@ -335,12 +418,23 @@ export default Store => {
|
|
|
335
418
|
|
|
336
419
|
Store.prototype.mcpAddBookmarkGroup = async function (args) {
|
|
337
420
|
const { store } = window
|
|
421
|
+
const title = (args || {}).title
|
|
422
|
+
if (!title || typeof title !== 'string') {
|
|
423
|
+
throw new Error('title is required')
|
|
424
|
+
}
|
|
425
|
+
const parentId = (args || {}).parentId
|
|
426
|
+
if (parentId) {
|
|
427
|
+
const parent = (store.bookmarkGroups || []).find(g => g.id === parentId)
|
|
428
|
+
if (!parent) {
|
|
429
|
+
throw new Error(`Parent bookmark group not found: ${parentId}`)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
338
432
|
const group = {
|
|
339
433
|
id: uid(),
|
|
340
|
-
title
|
|
434
|
+
title,
|
|
341
435
|
bookmarkIds: [],
|
|
342
436
|
bookmarkGroupIds: [],
|
|
343
|
-
level:
|
|
437
|
+
level: parentId ? 2 : 1
|
|
344
438
|
}
|
|
345
439
|
|
|
346
440
|
await store.addBookmarkGroup(group)
|
|
@@ -418,6 +512,8 @@ export default Store => {
|
|
|
418
512
|
host: t.host,
|
|
419
513
|
type: t.type || 'local',
|
|
420
514
|
status: t.status,
|
|
515
|
+
from: t.from,
|
|
516
|
+
mcpStatus: t.mcpStatus || '',
|
|
421
517
|
isTransporting: t.isTransporting,
|
|
422
518
|
onData: refsTabs.get('tab-' + t.id)?.state.terminalOnData,
|
|
423
519
|
batch: t.batch
|
|
@@ -511,6 +607,10 @@ export default Store => {
|
|
|
511
607
|
const { store } = window
|
|
512
608
|
store.addTab()
|
|
513
609
|
const newTabId = store.activeTabId
|
|
610
|
+
const tab = store.tabs.find(t => t.id === newTabId)
|
|
611
|
+
if (tab) {
|
|
612
|
+
tab.from = 'mcp'
|
|
613
|
+
}
|
|
514
614
|
|
|
515
615
|
return {
|
|
516
616
|
success: true,
|
|
@@ -524,7 +624,10 @@ export default Store => {
|
|
|
524
624
|
// Strip dangerous execution-related props before any processing.
|
|
525
625
|
// This prevents MCP/AI callers from injecting execLinux/execLinuxArgs,
|
|
526
626
|
// setEnv, runScripts, etc. to spawn arbitrary local processes.
|
|
527
|
-
|
|
627
|
+
// Also strip tab UI-state props (notably `batch`): a crafted batch
|
|
628
|
+
// used to crash Sessions via `sizes[batch]` undefined destructuring.
|
|
629
|
+
// Batch is always assigned by addTab from the current layout.
|
|
630
|
+
const safeArgs = stripTabInternalProps(stripDangerousTabProps({ ...(args || {}) }))
|
|
528
631
|
const data = fixBookmarkData(safeArgs)
|
|
529
632
|
|
|
530
633
|
const { valid, errors } = validateBookmarkData(data)
|
|
@@ -537,6 +640,7 @@ export default Store => {
|
|
|
537
640
|
from: 'mcp',
|
|
538
641
|
...newTerm(true, true)
|
|
539
642
|
}
|
|
643
|
+
delete tab.batch
|
|
540
644
|
|
|
541
645
|
store.addTab(tab)
|
|
542
646
|
const newTabId = store.activeTabId
|
|
@@ -565,6 +669,8 @@ export default Store => {
|
|
|
565
669
|
}
|
|
566
670
|
|
|
567
671
|
store.runQuickCommand(command, args.inputOnly || false, tabId)
|
|
672
|
+
// Show [running] on the tab; auto-clears if the caller never waits.
|
|
673
|
+
setTabMcpStatusAuto(tabId, 'running', 15000)
|
|
568
674
|
|
|
569
675
|
return {
|
|
570
676
|
success: true,
|
|
@@ -652,6 +758,7 @@ export default Store => {
|
|
|
652
758
|
throw new Error('No active terminal')
|
|
653
759
|
}
|
|
654
760
|
|
|
761
|
+
setTabMcpStatus(tabId, 'waiting')
|
|
655
762
|
const start = Date.now()
|
|
656
763
|
|
|
657
764
|
// Brief initial wait so the command has time to start producing output
|
|
@@ -684,6 +791,7 @@ export default Store => {
|
|
|
684
791
|
const onData = tabRef?.state.terminalOnData
|
|
685
792
|
if (!onData) {
|
|
686
793
|
const { output, lineCount } = collectOutput()
|
|
794
|
+
setTabMcpStatusAuto(tabId, 'done')
|
|
687
795
|
return {
|
|
688
796
|
tabId,
|
|
689
797
|
elapsed: Date.now() - start,
|
|
@@ -695,7 +803,9 @@ export default Store => {
|
|
|
695
803
|
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
696
804
|
}
|
|
697
805
|
|
|
698
|
-
// Timeout reached — return whatever is currently in the buffer
|
|
806
|
+
// Timeout reached — return whatever is currently in the buffer.
|
|
807
|
+
// The command is still producing output, so keep showing [running].
|
|
808
|
+
setTabMcpStatus(tabId, 'running')
|
|
699
809
|
const { output, lineCount } = collectOutput()
|
|
700
810
|
return {
|
|
701
811
|
tabId,
|
|
@@ -901,29 +1011,41 @@ export default Store => {
|
|
|
901
1011
|
let result = null
|
|
902
1012
|
let mode = 'pty'
|
|
903
1013
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1014
|
+
setTabMcpStatus(tabId, 'running')
|
|
1015
|
+
try {
|
|
1016
|
+
if (requestedMode === 'exec' && isSsh) {
|
|
1017
|
+
try {
|
|
1018
|
+
const r = await execCmd(tabId, command, timeoutMs)
|
|
1019
|
+
result = {
|
|
1020
|
+
stdout: r.stdout || '',
|
|
1021
|
+
stderr: r.stderr || '',
|
|
1022
|
+
stderrMerged: false,
|
|
1023
|
+
exitCode: typeof r.exitCode === 'number' ? r.exitCode : null,
|
|
1024
|
+
durationMs: Date.now() - start,
|
|
1025
|
+
timedOut: !!r.timedOut
|
|
1026
|
+
}
|
|
1027
|
+
mode = 'exec'
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
// Only session types that genuinely lack an exec channel
|
|
1030
|
+
// (local/telnet/serial/...) fall back to PTY sentinel capture.
|
|
1031
|
+
// An SSH tab always has exec support, so any exec failure there
|
|
1032
|
+
// means the connection is dead — surface it immediately as
|
|
1033
|
+
// [error] instead of polling a dead terminal for the full
|
|
1034
|
+
// timeout and misreporting timedOut.
|
|
1035
|
+
if (isSsh || !/not supported/i.test(e.message || '')) {
|
|
1036
|
+
throw e
|
|
1037
|
+
}
|
|
920
1038
|
}
|
|
921
1039
|
}
|
|
922
|
-
}
|
|
923
1040
|
|
|
924
|
-
|
|
925
|
-
|
|
1041
|
+
if (!result) {
|
|
1042
|
+
result = await store.mcpExecuteCommandPty({ command, tabId, timeoutMs })
|
|
1043
|
+
}
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
setTabMcpStatusAuto(tabId, 'error')
|
|
1046
|
+
throw err
|
|
926
1047
|
}
|
|
1048
|
+
setTabMcpStatusAuto(tabId, result.timedOut ? 'running' : 'done')
|
|
927
1049
|
|
|
928
1050
|
const out = truncateTail(result.stdout, maxOutputBytes)
|
|
929
1051
|
const err = truncateTail(result.stderr, maxOutputBytes)
|
|
@@ -995,6 +1117,7 @@ export default Store => {
|
|
|
995
1117
|
status: 'started'
|
|
996
1118
|
}
|
|
997
1119
|
backgroundTasks.set(taskId, task)
|
|
1120
|
+
setTabMcpStatus(task.tabId, 'running')
|
|
998
1121
|
|
|
999
1122
|
return {
|
|
1000
1123
|
taskId,
|
|
@@ -1027,6 +1150,7 @@ export default Store => {
|
|
|
1027
1150
|
task.status = 'completed'
|
|
1028
1151
|
task.exitCode = parseInt(exitCode, 10)
|
|
1029
1152
|
task.endTime = Date.now()
|
|
1153
|
+
setTabMcpStatusAuto(task.tabId, 'done')
|
|
1030
1154
|
return { ...task, status: 'completed', exitCode: task.exitCode }
|
|
1031
1155
|
}
|
|
1032
1156
|
return { ...task, status: 'unknown', message: 'PID file not found' }
|
|
@@ -1037,6 +1161,7 @@ export default Store => {
|
|
|
1037
1161
|
|
|
1038
1162
|
if (aliveCheck.trim() === 'alive') {
|
|
1039
1163
|
task.status = 'running'
|
|
1164
|
+
setTabMcpStatus(task.tabId, 'running')
|
|
1040
1165
|
return { ...task, pid, status: 'running' }
|
|
1041
1166
|
}
|
|
1042
1167
|
|
|
@@ -1048,6 +1173,7 @@ export default Store => {
|
|
|
1048
1173
|
task.status = 'completed'
|
|
1049
1174
|
task.exitCode = exitCode !== '' ? parseInt(exitCode, 10) : null
|
|
1050
1175
|
task.endTime = Date.now()
|
|
1176
|
+
setTabMcpStatusAuto(task.tabId, 'done')
|
|
1051
1177
|
return { ...task, pid, status: 'completed', exitCode: task.exitCode }
|
|
1052
1178
|
}
|
|
1053
1179
|
|
|
@@ -1083,6 +1209,7 @@ export default Store => {
|
|
|
1083
1209
|
`kill ${pid} 2>/dev/null; echo $? > ${task.exitFile}`)
|
|
1084
1210
|
task.status = 'cancelled'
|
|
1085
1211
|
task.endTime = Date.now()
|
|
1212
|
+
setTabMcpStatus(task.tabId, '')
|
|
1086
1213
|
return {
|
|
1087
1214
|
taskId: task.id,
|
|
1088
1215
|
pid,
|
|
@@ -1112,6 +1239,7 @@ export default Store => {
|
|
|
1112
1239
|
// best-effort remote cleanup
|
|
1113
1240
|
}
|
|
1114
1241
|
backgroundTasks.delete(args.taskId)
|
|
1242
|
+
setTabMcpStatus(task.tabId, '')
|
|
1115
1243
|
return { success: true, taskId: args.taskId }
|
|
1116
1244
|
}
|
|
1117
1245
|
|