@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.
Files changed (149) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +23 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +14 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/bookmark-form/index.jsx +10 -8
  29. package/client/components/common/item-filter.jsx +76 -0
  30. package/client/components/common/item-filter.styl +54 -0
  31. package/client/components/common/lazy-boundary.jsx +34 -0
  32. package/client/components/common/opacity.jsx +3 -1
  33. package/client/components/footer/footer-entry.jsx +35 -5
  34. package/client/components/footer/footer.styl +8 -1
  35. package/client/components/layout/layout.jsx +23 -31
  36. package/client/components/layout/pixed.js +3 -0
  37. package/client/components/main/main.jsx +80 -28
  38. package/client/components/main/term-fullscreen.styl +9 -3
  39. package/client/components/quick-commands/qm.styl +3 -2
  40. package/client/components/rdp/file-transfer.js +4 -4
  41. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  42. package/client/components/remote-monitor/monitor-details.styl +87 -0
  43. package/client/components/remote-monitor/monitor-model.js +764 -0
  44. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  45. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  46. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  47. package/client/components/remote-monitor/session-monitor.js +361 -0
  48. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  49. package/client/components/remote-monitor/visibility.js +18 -0
  50. package/client/components/session/session-control.jsx +29 -2
  51. package/client/components/session/session.jsx +40 -18
  52. package/client/components/session/sessions.jsx +5 -2
  53. package/client/components/setting-panel/col.jsx +32 -6
  54. package/client/components/setting-panel/list.styl +12 -0
  55. package/client/components/setting-panel/setting-common.jsx +25 -56
  56. package/client/components/setting-panel/setting-mobile-breadcrumb.jsx +29 -0
  57. package/client/components/setting-panel/setting-modal.jsx +64 -44
  58. package/client/components/setting-panel/setting-terminal.jsx +7 -2
  59. package/client/components/setting-panel/setting-wrap.jsx +0 -3
  60. package/client/components/setting-panel/setting-wrap.styl +80 -11
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  67. package/client/components/sidebar/index.jsx +28 -21
  68. package/client/components/sidebar/left-sidebar-icons.jsx +1 -1
  69. package/client/components/sidebar/sidebar.styl +16 -0
  70. package/client/components/sys-menu/menu-btn.jsx +30 -1
  71. package/client/components/tabs/index.jsx +2 -2
  72. package/client/components/tabs/tab-title.jsx +3 -2
  73. package/client/components/tabs/tab.jsx +5 -1
  74. package/client/components/tabs/tabs.styl +4 -0
  75. package/client/components/terminal/attach-addon-custom.js +295 -49
  76. package/client/components/terminal/automation/index.js +64 -0
  77. package/client/components/terminal/automation/keys.js +49 -0
  78. package/client/components/terminal/automation/stream-matcher.js +82 -0
  79. package/client/components/terminal/automation/strip-ansi.js +19 -0
  80. package/client/components/terminal/automation/trigger-engine.js +233 -0
  81. package/client/components/terminal/automation/trigger-presets.js +110 -0
  82. package/client/components/terminal/command-tracker-addon.js +31 -1
  83. package/client/components/terminal/mixins/index.js +27 -0
  84. package/client/components/terminal/mixins/term-attach.js +123 -0
  85. package/client/components/terminal/mixins/term-config.jsx +84 -0
  86. package/client/components/terminal/mixins/term-content.js +88 -0
  87. package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
  88. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  89. package/client/components/terminal/mixins/term-init.js +122 -0
  90. package/client/components/terminal/mixins/term-log.jsx +96 -0
  91. package/client/components/terminal/mixins/term-resize.js +87 -0
  92. package/client/components/terminal/mixins/term-socket.js +433 -0
  93. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  94. package/client/components/terminal/mixins/term-theme.js +190 -0
  95. package/client/components/terminal/mixins/term-touch.js +136 -0
  96. package/client/components/terminal/shell-detect.js +37 -0
  97. package/client/components/terminal/shell.js +0 -28
  98. package/client/components/terminal/shortcut-bar-entry.jsx +32 -27
  99. package/client/components/terminal/shortcut-bar.jsx +4 -6
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +56 -0
  102. package/client/components/terminal/startup-queue.js +379 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal.jsx +102 -2007
  105. package/client/components/terminal/transfer-client-base.js +5 -29
  106. package/client/components/terminal/trzsz-client.js +1 -3
  107. package/client/components/terminal/xmodem-client.js +1 -3
  108. package/client/components/terminal/xterm-loader.js +7 -0
  109. package/client/components/terminal/zmodem-client.js +1 -3
  110. package/client/components/terminal-info/base.jsx +18 -70
  111. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  112. package/client/components/terminal-info/terminal-info.jsx +50 -59
  113. package/client/components/terminal-info/terminal-info.styl +6 -49
  114. package/client/components/text-editor/open-text-editor.js +30 -0
  115. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  116. package/client/components/text-editor/text-editor.jsx +14 -0
  117. package/client/components/theme/theme-form.jsx +22 -0
  118. package/client/components/tree-list/bookmark-transport.jsx +7 -31
  119. package/client/components/tree-list/tree-list.styl +13 -0
  120. package/client/components/triggers/trigger-editor.jsx +246 -0
  121. package/client/components/triggers/trigger-form.jsx +200 -0
  122. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  123. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  124. package/client/components/triggers/trigger-transport.jsx +60 -0
  125. package/client/components/triggers/triggers-field.jsx +19 -0
  126. package/client/components/widgets/widgets-list.jsx +11 -1
  127. package/client/css/mobile.styl +9 -1
  128. package/client/store/common.js +12 -1
  129. package/client/store/init-state.js +14 -0
  130. package/client/store/mcp-handler.js +158 -30
  131. package/client/store/setting.js +20 -0
  132. package/client/store/sidebar.js +12 -0
  133. package/client/store/store.js +2 -0
  134. package/client/store/sync.js +45 -9
  135. package/client/store/system-menu.js +2 -0
  136. package/client/store/tab.js +41 -3
  137. package/client/store/trigger.js +118 -0
  138. package/client/store/watch.js +13 -5
  139. package/client/views/index.pug +3 -0
  140. package/package.json +1 -1
  141. package/client/components/terminal-info/activity.jsx +0 -57
  142. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  143. package/client/components/terminal-info/disk.jsx +0 -45
  144. package/client/components/terminal-info/format-disks.js +0 -45
  145. package/client/components/terminal-info/network.jsx +0 -121
  146. package/client/components/terminal-info/resource.jsx +0 -99
  147. package/client/components/terminal-info/run-cmd.jsx +0 -263
  148. package/client/components/terminal-info/sys-info.jsx +0 -56
  149. package/client/components/terminal-info/up.jsx +0 -17
@@ -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
  }
@@ -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
@@ -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)
@@ -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
- if (!isJSON(serverStr)) {
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
- if (!isJSON(str)) {
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
- if (!isJSON(str)) {
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 () {
@@ -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
- newTab.batch = batch ?? newTab.batch ?? window.openTabBatch ?? window.store.currentLayoutBatch
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
+ }
@@ -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
- await update(
55
- `${name}:order`,
56
- newOrder
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
 
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "5.3.15",
3
+ "version": "5.5.6",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -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
- }