@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
package/client/store/setting.js
CHANGED
|
@@ -40,6 +40,7 @@ export default Store => {
|
|
|
40
40
|
})
|
|
41
41
|
store.setSettingItem(item)
|
|
42
42
|
store.openSettingModal()
|
|
43
|
+
store.settingMobileView = 'content'
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
Store.prototype.handleOpenQuickCommandsSetting = function () {
|
|
@@ -128,6 +129,15 @@ export default Store => {
|
|
|
128
129
|
store.openSettingModal()
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
Store.prototype.openTriggers = function () {
|
|
133
|
+
const { store } = window
|
|
134
|
+
store.storeAssign({
|
|
135
|
+
settingTab: settingMap.triggers
|
|
136
|
+
})
|
|
137
|
+
store.setSettingItem({ id: '', name: settingMap.triggers })
|
|
138
|
+
store.openSettingModal()
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
Store.prototype.openSettingModal = function () {
|
|
132
142
|
const { store } = window
|
|
133
143
|
if (store.isSecondInstance) {
|
|
@@ -136,12 +146,20 @@ export default Store => {
|
|
|
136
146
|
)
|
|
137
147
|
}
|
|
138
148
|
store.showModal = modals.setting
|
|
149
|
+
// mobile always lands on the left col menu first
|
|
150
|
+
store.settingMobileView = 'menu'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Store.prototype.backToSettingMenu = function () {
|
|
154
|
+
window.store.settingMobileView = 'menu'
|
|
139
155
|
}
|
|
140
156
|
|
|
141
157
|
Store.prototype.hideSettingModal = function () {
|
|
142
158
|
const { store } = window
|
|
143
159
|
store.showModal = modals.hide
|
|
144
160
|
store.setSettingItem({})
|
|
161
|
+
// reset so a later open starts from the menu on mobile
|
|
162
|
+
store.settingMobileView = 'menu'
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
Store.prototype.loadFontList = async function () {
|
|
@@ -161,5 +179,7 @@ export default Store => {
|
|
|
161
179
|
settingTab
|
|
162
180
|
})
|
|
163
181
|
store.setSettingItem(item)
|
|
182
|
+
// a new tab means a new menu to pick from on mobile
|
|
183
|
+
store.settingMobileView = 'menu'
|
|
164
184
|
}
|
|
165
185
|
}
|
package/client/store/sidebar.js
CHANGED
|
@@ -19,6 +19,18 @@ export default Store => {
|
|
|
19
19
|
store.expandedKeys = []
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Single entry point for opening/closing the left side panel from the
|
|
23
|
+
// sidebar icons. The panel is a toggle: call again to close. It also
|
|
24
|
+
// re-centers the pin behaviour so every open/close transition goes
|
|
25
|
+
// through this one method.
|
|
26
|
+
Store.prototype.openLeftSidePanel = function (field = 'bookmarks') {
|
|
27
|
+
const { store } = window
|
|
28
|
+
if (store.pinned) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
store.setOpenedSideBar(store.openedSideBar ? '' : field)
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
Store.prototype.handlePin = function (pinned) {
|
|
23
35
|
const { store } = window
|
|
24
36
|
const current = !store.pinned
|
package/client/store/store.js
CHANGED
|
@@ -13,6 +13,7 @@ import bookmarkExtend from './bookmark'
|
|
|
13
13
|
import commonExtend from './common'
|
|
14
14
|
import itemExtend from './item'
|
|
15
15
|
import quickCommandExtend from './quick-command'
|
|
16
|
+
import triggerExtend from './trigger'
|
|
16
17
|
import sessionExtend from './session'
|
|
17
18
|
import settingExtend from './setting'
|
|
18
19
|
import sidebarExtend from './sidebar'
|
|
@@ -312,6 +313,7 @@ bookmarkExtend(Store)
|
|
|
312
313
|
commonExtend(Store)
|
|
313
314
|
itemExtend(Store)
|
|
314
315
|
quickCommandExtend(Store)
|
|
316
|
+
triggerExtend(Store)
|
|
315
317
|
sessionExtend(Store)
|
|
316
318
|
settingExtend(Store)
|
|
317
319
|
sidebarExtend(Store)
|
package/client/store/sync.js
CHANGED
|
@@ -52,6 +52,46 @@ function isJSON (str = '') {
|
|
|
52
52
|
return str.startsWith('[')
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Names of data types that are actually encrypted on upload when a
|
|
57
|
+
* sync password is set. Only these types trigger the fail-closed
|
|
58
|
+
* check on download.
|
|
59
|
+
*/
|
|
60
|
+
const encryptedDataNames = new Set([
|
|
61
|
+
settingMap.bookmarks,
|
|
62
|
+
settingMap.profiles
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decrypt sync data with fail-closed semantics.
|
|
67
|
+
*
|
|
68
|
+
* Only bookmarks and profiles are encrypted on upload when a sync
|
|
69
|
+
* password is configured. For those two types, plaintext JSON
|
|
70
|
+
* (content starting with '[') must never be silently accepted when a
|
|
71
|
+
* password is set — it could be attacker-injected data from a
|
|
72
|
+
* compromised sync backend. Instead, we reject it so the user is
|
|
73
|
+
* alerted rather than silently importing untrusted data.
|
|
74
|
+
*
|
|
75
|
+
* For all other data types, plaintext JSON is expected even when a
|
|
76
|
+
* password is set, so we accept it without error.
|
|
77
|
+
*/
|
|
78
|
+
async function decryptSyncData (str, pass, dataName) {
|
|
79
|
+
if (!str) {
|
|
80
|
+
return str
|
|
81
|
+
}
|
|
82
|
+
if (isJSON(str)) {
|
|
83
|
+
if (pass && encryptedDataNames.has(dataName)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'Sync data is plaintext but a sync password is configured. ' +
|
|
86
|
+
'This may indicate the data was tampered with or the sync backend is compromised. ' +
|
|
87
|
+
'Aborting sync download for safety.'
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return str
|
|
91
|
+
}
|
|
92
|
+
return window.pre.runGlobalAsync('decryptAsync', str, pass)
|
|
93
|
+
}
|
|
94
|
+
|
|
55
95
|
async function fetchData (type, func, args, token, proxy) {
|
|
56
96
|
const data = {
|
|
57
97
|
type,
|
|
@@ -337,9 +377,7 @@ export default (Store) => {
|
|
|
337
377
|
let serverItems = []
|
|
338
378
|
if (serverStr) {
|
|
339
379
|
try {
|
|
340
|
-
|
|
341
|
-
serverStr = await window.pre.runGlobalAsync('decryptAsync', serverStr, pass)
|
|
342
|
-
}
|
|
380
|
+
serverStr = await decryptSyncData(serverStr, pass, n)
|
|
343
381
|
serverItems = JSON.parse(serverStr)
|
|
344
382
|
} catch (e) {
|
|
345
383
|
console.error(`Failed to parse server data for ${n}:`, e)
|
|
@@ -520,9 +558,7 @@ export default (Store) => {
|
|
|
520
558
|
continue
|
|
521
559
|
}
|
|
522
560
|
}
|
|
523
|
-
|
|
524
|
-
str = await window.pre.runGlobalAsync('decryptAsync', str, pass)
|
|
525
|
-
}
|
|
561
|
+
str = await decryptSyncData(str, pass, n)
|
|
526
562
|
let arr = JSON.parse(str)
|
|
527
563
|
if (n === settingMap.terminalThemes) {
|
|
528
564
|
arr = store.fixThemes(arr)
|
|
@@ -585,9 +621,7 @@ export default (Store) => {
|
|
|
585
621
|
continue
|
|
586
622
|
}
|
|
587
623
|
}
|
|
588
|
-
|
|
589
|
-
str = await window.pre.runGlobalAsync('decryptAsync', str, pass)
|
|
590
|
-
}
|
|
624
|
+
str = await decryptSyncData(str, pass, n)
|
|
591
625
|
let arr = JSON.parse(
|
|
592
626
|
str
|
|
593
627
|
)
|
|
@@ -811,6 +845,8 @@ export default (Store) => {
|
|
|
811
845
|
'addTimeStampToTermLog',
|
|
812
846
|
'showHiddenFilesOnSftpStart',
|
|
813
847
|
'terminalInfos',
|
|
848
|
+
'remoteMonitorBarEnabled',
|
|
849
|
+
'remoteMonitorBarItems',
|
|
814
850
|
'filePropsEnabled',
|
|
815
851
|
'hideIP',
|
|
816
852
|
'terminalTimeout',
|
|
@@ -60,6 +60,8 @@ export default Store => {
|
|
|
60
60
|
})
|
|
61
61
|
store.setSettingItem(getInitItem([], settingMap.bookmarks))
|
|
62
62
|
store.openSettingModal()
|
|
63
|
+
// on mobile, jump straight to the new bookmark form instead of the menu
|
|
64
|
+
store.settingMobileView = 'content'
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
Store.prototype.onNewSshAI = function () {
|
package/client/store/tab.js
CHANGED
|
@@ -17,6 +17,13 @@ import generate from '../common/id-with-stamp'
|
|
|
17
17
|
import uid from '../common/uid'
|
|
18
18
|
import newTerm, { updateCount } from '../common/new-terminal.js'
|
|
19
19
|
import { action } from 'manate'
|
|
20
|
+
import { shouldCaptureSshReloadState } from '../components/terminal/ssh-reload-state.js'
|
|
21
|
+
|
|
22
|
+
function captureSshSessionState (tab, config) {
|
|
23
|
+
return shouldCaptureSshReloadState(tab, config)
|
|
24
|
+
? refs.get(`term-${tab.id}`)?.getReloadState?.()
|
|
25
|
+
: undefined
|
|
26
|
+
}
|
|
20
27
|
|
|
21
28
|
export default Store => {
|
|
22
29
|
Store.prototype.nextTabCount = function () {
|
|
@@ -96,6 +103,10 @@ export default Store => {
|
|
|
96
103
|
const oldTab = tabs[index]
|
|
97
104
|
const oldBatch = oldTab.batch
|
|
98
105
|
|
|
106
|
+
// Reload state is captured only for an SSH refresh/reconnect. Closing a
|
|
107
|
+
// tab goes through removeTabs and never reaches this code path.
|
|
108
|
+
const reloadState = captureSshSessionState(oldTab, store.config)
|
|
109
|
+
|
|
99
110
|
// Create copy of old tab with new ID
|
|
100
111
|
const newTab = {
|
|
101
112
|
...oldTab,
|
|
@@ -103,6 +114,10 @@ export default Store => {
|
|
|
103
114
|
id: generate(), // Need to create new ID
|
|
104
115
|
status: statusMap.processing // Reset status
|
|
105
116
|
}
|
|
117
|
+
delete newTab._reloadState
|
|
118
|
+
if (reloadState) {
|
|
119
|
+
newTab._reloadState = reloadState
|
|
120
|
+
}
|
|
106
121
|
|
|
107
122
|
// Add new tab at next index
|
|
108
123
|
tabs.splice(index + 1, 0, newTab)
|
|
@@ -153,6 +168,7 @@ export default Store => {
|
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
const sourceTab = tabs[targetIndex]
|
|
171
|
+
const sessionState = captureSshSessionState(sourceTab, store.config)
|
|
156
172
|
const duplicatedTab = {
|
|
157
173
|
...deepCopy(sourceTab),
|
|
158
174
|
id: generate(),
|
|
@@ -160,6 +176,10 @@ export default Store => {
|
|
|
160
176
|
status: statusMap.processing,
|
|
161
177
|
isTransporting: undefined
|
|
162
178
|
}
|
|
179
|
+
delete duplicatedTab._reloadState
|
|
180
|
+
if (sessionState) {
|
|
181
|
+
duplicatedTab._reloadState = sessionState
|
|
182
|
+
}
|
|
163
183
|
|
|
164
184
|
// Insert the duplicated tab after the source tab
|
|
165
185
|
tabs.splice(targetIndex + 1, 0, duplicatedTab)
|
|
@@ -361,7 +381,19 @@ export default Store => {
|
|
|
361
381
|
const { store } = window
|
|
362
382
|
const { tabs } = store
|
|
363
383
|
newTab.tabCount = store.nextTabCount()
|
|
364
|
-
|
|
384
|
+
// Sanitize batch: MCP/AI callers may pass a missing, string, or
|
|
385
|
+
// out-of-range batch which would crash Sessions (sizes[batch]
|
|
386
|
+
// undefined). Clamp to a valid pane index for the current layout.
|
|
387
|
+
let batchNum = batch ?? newTab.batch ?? window.openTabBatch ?? window.store.currentLayoutBatch
|
|
388
|
+
batchNum = Number(batchNum)
|
|
389
|
+
const maxBatch = (splitConfig[store.layout] && splitConfig[store.layout].children) || 1
|
|
390
|
+
if (!Number.isInteger(batchNum) || batchNum < 0 || batchNum >= maxBatch) {
|
|
391
|
+
batchNum = Number(window.store.currentLayoutBatch) || 0
|
|
392
|
+
if (!Number.isInteger(batchNum) || batchNum < 0 || batchNum >= maxBatch) {
|
|
393
|
+
batchNum = 0
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
newTab.batch = batchNum
|
|
365
397
|
if (!newTab.id) {
|
|
366
398
|
newTab.id = generate()
|
|
367
399
|
}
|
|
@@ -376,7 +408,6 @@ export default Store => {
|
|
|
376
408
|
} else {
|
|
377
409
|
tabs.push(newTab)
|
|
378
410
|
}
|
|
379
|
-
const batchNum = newTab.batch
|
|
380
411
|
store[`activeTabId${batchNum}`] = newTab.id
|
|
381
412
|
store.activeTabId = newTab.id
|
|
382
413
|
store.currentLayoutBatch = batchNum
|
|
@@ -393,7 +424,8 @@ export default Store => {
|
|
|
393
424
|
'execLinuxArgs',
|
|
394
425
|
'setEnv',
|
|
395
426
|
'runScripts',
|
|
396
|
-
'interactiveValues'
|
|
427
|
+
'interactiveValues',
|
|
428
|
+
'triggers'
|
|
397
429
|
]
|
|
398
430
|
|
|
399
431
|
Store.prototype.ipcOpenTab = function (parsed) {
|
|
@@ -454,12 +486,17 @@ export default Store => {
|
|
|
454
486
|
const defaultStatus = statusMap.processing
|
|
455
487
|
const { layout, currentLayoutBatch } = store
|
|
456
488
|
const ntb = deepCopy(tab)
|
|
489
|
+
const sessionState = captureSshSessionState(tab, store.config)
|
|
457
490
|
Object.assign(ntb, {
|
|
458
491
|
id: generate(),
|
|
459
492
|
status: defaultStatus,
|
|
460
493
|
isTransporting: undefined,
|
|
461
494
|
pane: paneMap.terminal
|
|
462
495
|
})
|
|
496
|
+
delete ntb._reloadState
|
|
497
|
+
if (sessionState) {
|
|
498
|
+
ntb._reloadState = sessionState
|
|
499
|
+
}
|
|
463
500
|
let maxBatch = splitConfig[layout].children
|
|
464
501
|
if (maxBatch < 2) {
|
|
465
502
|
maxBatch = 2
|
|
@@ -548,6 +585,7 @@ export default Store => {
|
|
|
548
585
|
'sshTunnelResults',
|
|
549
586
|
'displayRaw',
|
|
550
587
|
'autoReConnect',
|
|
588
|
+
'_reloadState',
|
|
551
589
|
'isPinned'
|
|
552
590
|
]
|
|
553
591
|
const { history } = store
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* declarative auto triggers (ZocRespond style) related functions
|
|
3
|
+
* global triggers live in store.triggers (persisted via db watch),
|
|
4
|
+
* bookmark triggers live on bookmark/tab `triggers` field
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { settingMap } from '../common/constants'
|
|
8
|
+
import { refs } from '../components/common/ref'
|
|
9
|
+
import { validateTriggers } from '../components/terminal/automation/trigger-engine.js'
|
|
10
|
+
|
|
11
|
+
export function normalizeTrigger (t = {}) {
|
|
12
|
+
return {
|
|
13
|
+
id: t.id || '',
|
|
14
|
+
name: t.name || '',
|
|
15
|
+
enabled: t.enabled !== false,
|
|
16
|
+
match: {
|
|
17
|
+
type: t.match?.type === 'regex' ? 'regex' : 'text',
|
|
18
|
+
value: t.match?.value || '',
|
|
19
|
+
caseSensitive: !!t.match?.caseSensitive
|
|
20
|
+
},
|
|
21
|
+
action: {
|
|
22
|
+
type: t.action?.type === 'notify' ? 'notify' : 'send',
|
|
23
|
+
value: t.action?.value || ''
|
|
24
|
+
},
|
|
25
|
+
sendEnter: t.sendEnter !== false,
|
|
26
|
+
mode: ['repeat', 'once', 'cooldown'].includes(t.mode) ? t.mode : 'cooldown',
|
|
27
|
+
cooldownMs: Number.isFinite(+t.cooldownMs) ? +t.cooldownMs : 500
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default Store => {
|
|
32
|
+
Store.prototype.addTrigger = function (trigger) {
|
|
33
|
+
window.store.addItem(normalizeTrigger(trigger), settingMap.triggers)
|
|
34
|
+
window.store.refreshAllTriggers()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Store.prototype.editTrigger = function (id, update) {
|
|
38
|
+
window.store.editItem(id, normalizeTrigger({ ...window.store.triggers.find(t => t.id === id), ...update }), settingMap.triggers)
|
|
39
|
+
window.store.refreshAllTriggers()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Store.prototype.delTrigger = function ({ id }) {
|
|
43
|
+
window.store.delItem({ id }, settingMap.triggers)
|
|
44
|
+
window.store.refreshAllTriggers()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Store.prototype.setTriggers = function (list) {
|
|
48
|
+
const arr = (list || []).map(normalizeTrigger)
|
|
49
|
+
const errors = validateTriggers(arr)
|
|
50
|
+
if (errors.length) {
|
|
51
|
+
return errors
|
|
52
|
+
}
|
|
53
|
+
window.store.setItems(settingMap.triggers, arr)
|
|
54
|
+
window.store.refreshAllTriggers()
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// effective triggers for a tab: predefined (global) triggers resolved with
|
|
59
|
+
// per-session on/off overrides + tab session/bookmark triggers.
|
|
60
|
+
// Only predefined triggers persist (db, synced); bookmark triggers stay on
|
|
61
|
+
// the bookmark record; session temp rules live on the in-memory tab only.
|
|
62
|
+
Store.prototype.getEffectiveTriggers = function (tab) {
|
|
63
|
+
const overrides = (tab && tab.triggerOverrides) || {}
|
|
64
|
+
const globals = (window.store.triggers || [])
|
|
65
|
+
.filter(t => t && t.match && t.match.value)
|
|
66
|
+
.map(t => {
|
|
67
|
+
const on = t.id in overrides ? overrides[t.id] : t.enabled !== false
|
|
68
|
+
return normalizeTrigger({ ...t, enabled: on })
|
|
69
|
+
})
|
|
70
|
+
const session = (tab && tab.triggers) || []
|
|
71
|
+
return [...globals, ...session.map(normalizeTrigger)]
|
|
72
|
+
.filter(t => t.enabled !== false && t.match.value)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// per-session on/off for a predefined trigger (memory only, not persisted)
|
|
76
|
+
Store.prototype.togglePredefinedTrigger = function (tabId, triggerId, enabled) {
|
|
77
|
+
const id = tabId || window.store.activeTabId
|
|
78
|
+
const tab = window.store.tabs.find(t => t.id === id)
|
|
79
|
+
if (!tab) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const overrides = { ...(tab.triggerOverrides || {}) }
|
|
83
|
+
overrides[triggerId] = !!enabled
|
|
84
|
+
window.store.updateTab(id, { triggerOverrides: overrides })
|
|
85
|
+
refs.get('term-' + id)?.refreshTriggers?.()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Store.prototype.getSessionTriggers = function (tabId) {
|
|
89
|
+
const tab = window.store.tabs.find(t => t.id === (tabId || window.store.activeTabId))
|
|
90
|
+
return (tab && tab.triggers) || []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Store.prototype.setSessionTriggers = function (tabId, list) {
|
|
94
|
+
const id = tabId || window.store.activeTabId
|
|
95
|
+
const arr = (list || []).map(normalizeTrigger)
|
|
96
|
+
const errors = validateTriggers(arr.filter(t => t.match.value || t.name))
|
|
97
|
+
if (errors.length) {
|
|
98
|
+
return errors
|
|
99
|
+
}
|
|
100
|
+
window.store.updateTab(id, { triggers: arr })
|
|
101
|
+
refs.get('term-' + id)?.refreshTriggers?.()
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Store.prototype.refreshAllTriggers = function () {
|
|
106
|
+
for (const tab of window.store.tabs || []) {
|
|
107
|
+
try {
|
|
108
|
+
refs.get('term-' + tab.id)?.refreshTriggers?.()
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.debug(e)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Store.prototype.toggleTriggerSessionModal = function (open = null) {
|
|
116
|
+
window.store.triggerSessionOpen = open == null ? !window.store.triggerSessionOpen : !!open
|
|
117
|
+
}
|
|
118
|
+
}
|
package/client/store/watch.js
CHANGED
|
@@ -21,6 +21,12 @@ import dataCompare from '../common/data-compare'
|
|
|
21
21
|
|
|
22
22
|
export default store => {
|
|
23
23
|
for (const name of dbNamesForWatch) {
|
|
24
|
+
// ai chat history changes on every streaming tick (200ms polls,
|
|
25
|
+
// per-chunk agent updates); debounce to coalesce them into one
|
|
26
|
+
// db write after the stream pauses
|
|
27
|
+
const schedule = name === 'aiChatHistory'
|
|
28
|
+
? func => debounce(func, 1000)
|
|
29
|
+
: undefined
|
|
24
30
|
window[`watch${name}`] = autoRun(async () => {
|
|
25
31
|
const n = store.getItems(name)
|
|
26
32
|
if (window.migrating) {
|
|
@@ -51,10 +57,12 @@ export default store => {
|
|
|
51
57
|
...updated.map(item => update(item.id, item, name, false)),
|
|
52
58
|
added.length ? insert(name, added) : Promise.resolve()
|
|
53
59
|
])
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
if (orderChanged) {
|
|
61
|
+
await update(
|
|
62
|
+
`${name}:order`,
|
|
63
|
+
newOrder
|
|
64
|
+
)
|
|
65
|
+
}
|
|
58
66
|
if (name === 'bookmarks') {
|
|
59
67
|
store.bookmarksMap = new Map(
|
|
60
68
|
n.map(d => [d.id, d])
|
|
@@ -73,7 +81,7 @@ export default store => {
|
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
return store[name]
|
|
76
|
-
})
|
|
84
|
+
}, schedule)
|
|
77
85
|
window[`watch${name}`].start()
|
|
78
86
|
}
|
|
79
87
|
|
package/client/views/index.pug
CHANGED
|
@@ -23,6 +23,9 @@ html
|
|
|
23
23
|
}
|
|
24
24
|
.electerm-logo-bg {
|
|
25
25
|
background: transparent 50% 50% no-repeat url("./images/electerm-watermark.png");
|
|
26
|
+
/* the watermark is 766 x 266; without a size it is cropped on narrow
|
|
27
|
+
panes (mobile), so scale it down to fit the pane width instead */
|
|
28
|
+
background-size: min(100%, 766px) auto;
|
|
26
29
|
}
|
|
27
30
|
.morph-shape {
|
|
28
31
|
background: linear-gradient(45deg, #08c 0%, #09c 100%);
|
package/package.json
CHANGED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* process cpu/mem activities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Table, Tooltip, Popconfirm } from 'antd'
|
|
6
|
-
import { isEmpty } from 'lodash-es'
|
|
7
|
-
import { CloseCircleOutlined, BarsOutlined } from '@ant-design/icons'
|
|
8
|
-
import colsParser from './data-cols-parser'
|
|
9
|
-
|
|
10
|
-
const e = window.translate
|
|
11
|
-
|
|
12
|
-
export default function TerminalInfoActivities (props) {
|
|
13
|
-
const { activities, isRemote, terminalInfos } = props
|
|
14
|
-
if (isEmpty(activities) || !isRemote || !terminalInfos.includes('activities')) {
|
|
15
|
-
return null
|
|
16
|
-
}
|
|
17
|
-
const col = colsParser(activities[0])
|
|
18
|
-
col.unshift({
|
|
19
|
-
dataIndex: 'kill',
|
|
20
|
-
key: 'kill',
|
|
21
|
-
title: e('close'),
|
|
22
|
-
render: (txt, inst) => {
|
|
23
|
-
return (
|
|
24
|
-
<Tooltip
|
|
25
|
-
title={e('close')}
|
|
26
|
-
>
|
|
27
|
-
<Popconfirm
|
|
28
|
-
title={e('close') + ' pid:' + inst.pid + ' ?'}
|
|
29
|
-
onConfirm={() => props.killProcess(inst.pid)}
|
|
30
|
-
>
|
|
31
|
-
<CloseCircleOutlined
|
|
32
|
-
className='pointer'
|
|
33
|
-
/>
|
|
34
|
-
</Popconfirm>
|
|
35
|
-
</Tooltip>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
const ps = {
|
|
40
|
-
rowKey: 'pid',
|
|
41
|
-
dataSource: activities,
|
|
42
|
-
bordered: true,
|
|
43
|
-
columns: col,
|
|
44
|
-
size: 'small',
|
|
45
|
-
pagination: false,
|
|
46
|
-
scroll: {
|
|
47
|
-
y: 180,
|
|
48
|
-
x: 'max-content'
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return (
|
|
52
|
-
<div className='terminal-info-section terminal-info-act'>
|
|
53
|
-
<div className='pd1y bold'><BarsOutlined /> Activities</div>
|
|
54
|
-
<Table {...ps} />
|
|
55
|
-
</div>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { copy } from '../../common/clipboard'
|
|
2
|
-
import filesizeParser from 'filesize-parser'
|
|
3
|
-
import { filesize } from 'filesize'
|
|
4
|
-
|
|
5
|
-
const valueParserMaps = {
|
|
6
|
-
size: v => v,
|
|
7
|
-
used: filesizeParser,
|
|
8
|
-
avail: filesizeParser,
|
|
9
|
-
usedPercent: v => parseFloat(v.replace('%', ''))
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function valueParse (obj, k) {
|
|
13
|
-
if (valueParserMaps[k]) {
|
|
14
|
-
return valueParserMaps[k](obj[k])
|
|
15
|
-
}
|
|
16
|
-
return obj[k]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function copyValue (event) {
|
|
20
|
-
copy(event.target.getAttribute('data-content'))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export default (data) => {
|
|
24
|
-
return Object.keys(data).map(k => {
|
|
25
|
-
const rd = (txt) => {
|
|
26
|
-
// txt is in KB, convert to bytes for filesize function
|
|
27
|
-
const parsed = parseInt(txt, 10)
|
|
28
|
-
const r = k === 'mem' ? filesize(Number.isFinite(parsed) ? parsed * 1024 : 0) : txt
|
|
29
|
-
const itemProps = {
|
|
30
|
-
className: 'activity-item pointer',
|
|
31
|
-
'data-content': r,
|
|
32
|
-
onClick: copyValue
|
|
33
|
-
}
|
|
34
|
-
return (
|
|
35
|
-
<div
|
|
36
|
-
{...itemProps}
|
|
37
|
-
>
|
|
38
|
-
{r}
|
|
39
|
-
</div>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
title: k,
|
|
44
|
-
dataIndex: k,
|
|
45
|
-
key: k,
|
|
46
|
-
sorter: (a, b) => {
|
|
47
|
-
const va = valueParse(a, k)
|
|
48
|
-
const vb = valueParse(b, k)
|
|
49
|
-
return va > vb ? 1 : -1
|
|
50
|
-
},
|
|
51
|
-
render: rd
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* disk info
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Table } from 'antd'
|
|
6
|
-
import { isEmpty } from 'lodash-es'
|
|
7
|
-
import colsParser from './data-cols-parser'
|
|
8
|
-
import { PartitionOutlined } from '@ant-design/icons'
|
|
9
|
-
|
|
10
|
-
export default function TerminalInfoDisk (props) {
|
|
11
|
-
const { disks, isRemote, terminalInfos } = props
|
|
12
|
-
if (isEmpty(disks) || !isRemote || !terminalInfos.includes('disks')) {
|
|
13
|
-
return null
|
|
14
|
-
}
|
|
15
|
-
const col = colsParser(disks[0])
|
|
16
|
-
disks.sort((a, b) => {
|
|
17
|
-
const af = a.filesystem || ''
|
|
18
|
-
const bf = b.filesystem || ''
|
|
19
|
-
if (af.startsWith('/') && !bf.startsWith('/')) {
|
|
20
|
-
return -1
|
|
21
|
-
}
|
|
22
|
-
if (!af.startsWith('/') && bf.startsWith('/')) {
|
|
23
|
-
return 1
|
|
24
|
-
}
|
|
25
|
-
return 0
|
|
26
|
-
})
|
|
27
|
-
const ps = {
|
|
28
|
-
rowKey: (rec) => `${rec.mount}_${rec.filesystem}`,
|
|
29
|
-
dataSource: disks,
|
|
30
|
-
bordered: true,
|
|
31
|
-
columns: col,
|
|
32
|
-
size: 'small',
|
|
33
|
-
pagination: false,
|
|
34
|
-
scroll: {
|
|
35
|
-
y: 180,
|
|
36
|
-
x: 'max-content'
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return (
|
|
40
|
-
<div className='terminal-info-section terminal-info-disk'>
|
|
41
|
-
<div className='pd1y bold'><PartitionOutlined /> File system</div>
|
|
42
|
-
<Table {...ps} />
|
|
43
|
-
</div>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* parse `df` output into disk list
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export default function formatDisks (str) {
|
|
6
|
-
if (!str) {
|
|
7
|
-
return {
|
|
8
|
-
disks: []
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
const r = str.split('\n')
|
|
12
|
-
.slice(1)
|
|
13
|
-
.reduce((acc, s) => {
|
|
14
|
-
if (!s.trim()) {
|
|
15
|
-
return acc
|
|
16
|
-
}
|
|
17
|
-
const arr = s.trim().split(/ +/)
|
|
18
|
-
// df wraps long filesystem names (e.g. nfs "host:/very/long/export/path")
|
|
19
|
-
// onto a second line that starts with spaces and only holds the numeric
|
|
20
|
-
// columns. Merge such continuation lines back into the previous entry, so
|
|
21
|
-
// those disks (commonly nfs) are listed instead of dropped.
|
|
22
|
-
if (/^\s/.test(s) && acc.length) {
|
|
23
|
-
const prev = acc[acc.length - 1]
|
|
24
|
-
prev.size = arr[0]
|
|
25
|
-
prev.used = arr[1]
|
|
26
|
-
prev.avail = arr[2]
|
|
27
|
-
prev.usedPercent = arr[3]
|
|
28
|
-
prev.mount = arr[4]
|
|
29
|
-
return acc
|
|
30
|
-
}
|
|
31
|
-
acc.push({
|
|
32
|
-
filesystem: arr[0],
|
|
33
|
-
size: arr[1],
|
|
34
|
-
used: arr[2],
|
|
35
|
-
avail: arr[3],
|
|
36
|
-
usedPercent: arr[4],
|
|
37
|
-
mount: arr[5]
|
|
38
|
-
})
|
|
39
|
-
return acc
|
|
40
|
-
}, [])
|
|
41
|
-
.filter(d => d.filesystem && d.mount)
|
|
42
|
-
return {
|
|
43
|
-
disks: r
|
|
44
|
-
}
|
|
45
|
-
}
|