@electerm/electerm-react 5.5.35 → 5.5.66

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 (54) hide show
  1. package/client/common/bookmark-schemas.js +1 -0
  2. package/client/common/constants.js +1 -0
  3. package/client/common/dangerous-session-fields.js +39 -0
  4. package/client/common/parse-quick-connect.js +47 -6
  5. package/client/common/parse-widget-log.js +57 -0
  6. package/client/common/safe-join.js +33 -0
  7. package/client/common/sanitize-filename.js +8 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +7 -3
  9. package/client/components/ai/agent.js +176 -33
  10. package/client/components/ai/ai-auto-compress.js +119 -0
  11. package/client/components/ai/ai-chat-history-item.jsx +125 -40
  12. package/client/components/ai/ai-chat.jsx +67 -19
  13. package/client/components/ai/ai-compress.js +95 -0
  14. package/client/components/ai/ai-config.jsx +29 -16
  15. package/client/components/ai/ai-config.styl +20 -0
  16. package/client/components/ai/ai-context.js +1 -1
  17. package/client/components/ai/ai-output.jsx +1 -6
  18. package/client/components/ai/ai-presets.js +0 -15
  19. package/client/components/batch-op/batch-op-runner.jsx +1 -0
  20. package/client/components/bookmark-form/bookmark-schema.js +1 -0
  21. package/client/components/bookmark-form/common/ssh-agent.jsx +31 -21
  22. package/client/components/bookmark-form/config/ssh.js +1 -0
  23. package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
  24. package/client/components/file-transfer/remote2remote-handlers.jsx +8 -3
  25. package/client/components/file-transfer/transfer.jsx +3 -119
  26. package/client/components/main/main.jsx +1 -0
  27. package/client/components/session/session-control.jsx +35 -2
  28. package/client/components/session/session-control.styl +5 -0
  29. package/client/components/session/session.jsx +13 -0
  30. package/client/components/setting-panel/setting-terminal.jsx +1 -0
  31. package/client/components/setting-panel/tab-widgets.jsx +27 -7
  32. package/client/components/sftp/file-item.jsx +0 -13
  33. package/client/components/terminal/mixins/term-init.js +1 -0
  34. package/client/components/terminal/mixins/term-touch.js +18 -0
  35. package/client/components/terminal/shell.js +11 -3
  36. package/client/components/terminal/shortcut-bar-press.js +192 -0
  37. package/client/components/terminal/shortcut-bar.jsx +199 -4
  38. package/client/components/terminal/shortcut-bar.styl +40 -21
  39. package/client/components/terminal/terminal.jsx +4 -0
  40. package/client/components/widgets/widget-control.jsx +4 -2
  41. package/client/components/widgets/widget-instance-detail.jsx +370 -0
  42. package/client/components/widgets/widget-instance.jsx +30 -13
  43. package/client/components/widgets/widget-instances.jsx +3 -1
  44. package/client/components/widgets/widgets-list.jsx +8 -0
  45. package/client/components/widgets/widgets.styl +100 -0
  46. package/client/store/common.js +81 -23
  47. package/client/store/init-state.js +7 -0
  48. package/client/store/load-data.js +1 -0
  49. package/client/store/mcp-handler.js +8 -15
  50. package/client/store/sync.js +1 -0
  51. package/client/store/tab.js +2 -15
  52. package/client/store/widgets.js +23 -5
  53. package/package.json +1 -1
  54. package/client/components/file-transfer/zip.js +0 -42
@@ -6,6 +6,7 @@ import handleError from '../common/error-handler'
6
6
  import Modal from '../components/common/modal'
7
7
  import { appendMandatoryGuardrails } from '../components/ai/ai-guardrails'
8
8
  import { buildSessionMessages } from '../components/ai/ai-context'
9
+ import { COMPRESS_SUMMARY_PROMPT } from '../components/ai/ai-auto-compress'
9
10
  import { debounce, some, get, pickBy } from 'lodash-es'
10
11
  import {
11
12
  leftSidePanelWidthKey,
@@ -17,6 +18,7 @@ import {
17
18
  addPanelWidthLsKey,
18
19
  connectionMap,
19
20
  lastAiChatSessionIdKey,
21
+ aiAutoCompressLsKey,
20
22
  mobileBreakpoint,
21
23
  splitMap,
22
24
  settingAiId,
@@ -444,6 +446,23 @@ export default Store => {
444
446
  })
445
447
  }
446
448
 
449
+ // Update one entry immutably. Both halves matter:
450
+ // - the store only notifies subscribers on a property write, so mutating the
451
+ // entry in place leaves the panel rendering what it already had;
452
+ // - the transcript is memo()'d per entry, and a memo() comparison of an entry
453
+ // that was mutated in place compares the object with itself, so React can
454
+ // never see the change and the entry stops updating.
455
+ Store.prototype.updateAiHistoryEntry = function (id, updates) {
456
+ const { aiChatHistory } = window.store
457
+ const index = aiChatHistory.findIndex(d => d.id === id)
458
+ if (index === -1) {
459
+ return
460
+ }
461
+ const next = [...aiChatHistory]
462
+ next[index] = { ...next[index], ...updates }
463
+ window.store.aiChatHistory = next
464
+ }
465
+
447
466
  Store.prototype.removeAiHistory = function (id) {
448
467
  const { store } = window
449
468
  const index = store.aiChatHistory.findIndex(d => d.id === id)
@@ -486,9 +505,15 @@ export default Store => {
486
505
  store.startNewChat()
487
506
  })
488
507
 
508
+ // One compression per session at a time. The placeholder entry is published
509
+ // before the summary arrives, so a second call (auto compress firing while a
510
+ // manual one is still waiting on the provider) would summarize the
511
+ // placeholder and append a second one.
512
+ const compressingSessions = new Set()
513
+
489
514
  Store.prototype.compressChatSession = async function (sessionId) {
490
515
  const { store } = window
491
- if (!sessionId) {
516
+ if (!sessionId || compressingSessions.has(sessionId)) {
492
517
  return
493
518
  }
494
519
  const sessionEntries = store.aiChatHistory
@@ -523,32 +548,18 @@ export default Store => {
523
548
  role: appendMandatoryGuardrails(firstEntry.roleAI + `;用[${lang}]回复`)
524
549
  })
525
550
 
526
- const summaryPrompt = 'Please summarize the above conversation concisely. Include key information, decisions, context, and any important details that would be needed to continue this conversation effectively.'
527
- messages.push({ role: 'user', content: summaryPrompt })
528
-
529
- const aiResponse = await window.pre.runGlobalAsync(
530
- 'AIchat',
531
- summaryPrompt,
532
- firstEntry.modelAI,
533
- firstEntry.roleAI,
534
- firstEntry.baseURLAI,
535
- firstEntry.apiPathAI,
536
- firstEntry.apiKeyAI,
537
- firstEntry.proxyAI,
538
- false,
539
- firstEntry.authHeaderNameAI,
540
- messages
541
- )
542
-
543
- if (aiResponse && aiResponse.error) {
544
- return store.onError(new Error(aiResponse.error))
545
- }
551
+ // The same closing request the agent loop's auto compression sends (see
552
+ // components/ai/ai-compress.js), so both paths summarize alike.
553
+ messages.push({ role: 'user', content: COMPRESS_SUMMARY_PROMPT })
546
554
 
547
- const summary = aiResponse.response || ''
555
+ // Show the entry before asking for the summary, and fill it in when the
556
+ // answer arrives. The request takes as long as any other AI request (much
557
+ // longer for a nearly full session), and a click that changes nothing on
558
+ // screen for that long reads as "compress did not work".
548
559
  const compressedEntry = {
549
560
  id: uid(),
550
561
  prompt: '*Compressed session summary*',
551
- response: summary,
562
+ response: '*Summarizing the session…*',
552
563
  isStreaming: false,
553
564
  pending: false,
554
565
  sessionId: null,
@@ -572,8 +583,55 @@ export default Store => {
572
583
  // push for the same reason as removeAiHistory -- the panel has to see the
573
584
  // new context size, which is the whole point of compressing.
574
585
  store.aiChatHistory = [...store.aiChatHistory, compressedEntry]
586
+ compressingSessions.add(sessionId)
587
+
588
+ // Anything short of a usable summary takes the placeholder away again: an
589
+ // empty summary must never replace the conversation, and a half finished
590
+ // one must not look like a result.
591
+ const fail = (msg) => {
592
+ store.removeAiHistory(compressedEntry.id)
593
+ return store.onError(new Error(msg))
594
+ }
595
+
596
+ try {
597
+ const aiResponse = await window.pre.runGlobalAsync(
598
+ 'AIchat',
599
+ COMPRESS_SUMMARY_PROMPT,
600
+ firstEntry.modelAI,
601
+ firstEntry.roleAI,
602
+ firstEntry.baseURLAI,
603
+ firstEntry.apiPathAI,
604
+ firstEntry.apiKeyAI,
605
+ firstEntry.proxyAI,
606
+ false,
607
+ firstEntry.authHeaderNameAI,
608
+ messages
609
+ )
610
+
611
+ if (aiResponse && aiResponse.error) {
612
+ return fail(aiResponse.error)
613
+ }
614
+
615
+ const summary = (aiResponse && aiResponse.response) || ''
616
+ if (!summary.trim()) {
617
+ return fail('Compression failed: empty summary from AI')
618
+ }
619
+ store.updateAiHistoryEntry(compressedEntry.id, { response: summary })
620
+ } finally {
621
+ compressingSessions.delete(sessionId)
622
+ }
575
623
  }
576
624
 
625
+ // Auto compression, both modes: ask turns and the agent loop read this flag
626
+ // before every request (see components/ai/ai-compress.js), and the panel's
627
+ // auto compress popover writes it. Persisted so the choice survives a reload.
628
+ Store.prototype.setAiAutoCompress = action(function (val) {
629
+ const { store } = window
630
+ const on = !!val
631
+ store.aiAutoCompress = on
632
+ ls.setItem(aiAutoCompressLsKey, on ? 'true' : 'false')
633
+ })
634
+
577
635
  Store.prototype.toggleChatSessions = action(function () {
578
636
  const { store } = window
579
637
  store.showChatSessions = !store.showChatSessions
@@ -27,6 +27,7 @@ import {
27
27
  syncServerDataKey,
28
28
  splitMap,
29
29
  lastAiChatSessionIdKey,
30
+ aiAutoCompressLsKey,
30
31
  mobileBreakpoint
31
32
  } from '../common/constants'
32
33
  import * as ls from '../common/safe-local-storage'
@@ -97,6 +98,10 @@ export default () => {
97
98
  _batchInputSelectedTabIds: new Set(),
98
99
  aiChatHistory: [],
99
100
  agentRunning: false,
101
+ // ask turns and the agent loop compact their conversation when the window
102
+ // gets full; persisted here so the toggle survives a reload
103
+ // (see components/ai/ai-compress.js)
104
+ aiAutoCompress: ls.getItem(aiAutoCompressLsKey) === 'true',
100
105
  // live context accounting while an agent loop is running: the agent
101
106
  // carries tool schemas and tool results that the session history does
102
107
  // not, so it publishes its own figure here (see components/ai/agent.js)
@@ -220,6 +225,8 @@ export default () => {
220
225
  exePath: '',
221
226
  isPortable: false,
222
227
  installSrc: '',
228
+ // where running widgets write their logs, from the main process
229
+ widgetLogPath: '',
223
230
  showSshConfigModal: false,
224
231
 
225
232
  // batch inputs
@@ -276,6 +276,7 @@ export default (Store) => {
276
276
  store.appPath = globs.appPath
277
277
  store.exePath = globs.exePath
278
278
  store.isPortable = globs.isPortable
279
+ store.widgetLogPath = globs.widgetLogPath || ''
279
280
  store._config = globs.config
280
281
  window.et.langs = globs.langs
281
282
  store.zoom(store.config.zoom, false, true)
@@ -17,23 +17,16 @@ import {
17
17
  validateBookmarkData
18
18
  } from '../components/bookmark-form/fix-bookmark-default'
19
19
  import newTerm from '../common/new-terminal'
20
+ import dangerousSessionFields from '../common/dangerous-session-fields'
20
21
 
21
22
  // Dangerous props that must not be accepted from MCP/AI tool calls.
22
- // Mirrors the blocklist in src/client/store/tab.js (dangerousTabProps).
23
- // These allow arbitrary command execution if set (e.g. execLinux/execLinuxArgs
24
- // override the shell binary, setEnv injects environment variables, runScripts
25
- // executes scripts, interactiveValues injects interactive prompts).
26
- const dangerousTabProps = [
27
- 'execLinux',
28
- 'execMac',
29
- 'execWindows',
30
- 'execWindowsArgs',
31
- 'execMacArgs',
32
- 'execLinuxArgs',
33
- 'setEnv',
34
- 'runScripts',
35
- 'interactiveValues'
36
- ]
23
+ // Shared with src/client/store/tab.js and the quick connect parser so the
24
+ // copies cannot drift apart. These allow arbitrary command execution if set
25
+ // (execLinux/execLinuxArgs override the shell binary, setEnv injects
26
+ // environment variables, runScripts executes scripts, interactiveValues
27
+ // injects interactive prompts, triggers auto-answer terminal output,
28
+ // proxyCommand is spawned before any SSH authentication).
29
+ const dangerousTabProps = dangerousSessionFields
37
30
 
38
31
  // Tab/session UI state keys that must never be set by MCP/AI callers.
39
32
  // A crafted `batch` (missing, string, or out-of-range) used to crash the
@@ -819,6 +819,7 @@ export default (Store) => {
819
819
  const configSyncKeys = [
820
820
  'keepaliveInterval',
821
821
  'rightClickSelectsWord',
822
+ 'mouseEventsRequireAlt',
822
823
  'pasteWhenContextMenu',
823
824
  'ctrlOrMetaOpenTerminalLink',
824
825
  'hotkey',
@@ -18,6 +18,7 @@ import uid from '../common/uid'
18
18
  import newTerm, { updateCount } from '../common/new-terminal.js'
19
19
  import { action } from 'manate'
20
20
  import { shouldCaptureTerminalReloadState } from '../components/terminal/ssh-reload-state.js'
21
+ import dangerousSessionFields from '../common/dangerous-session-fields'
21
22
 
22
23
  function captureSshSessionState (tab, config) {
23
24
  return shouldCaptureTerminalReloadState(tab, config)
@@ -415,23 +416,9 @@ export default Store => {
415
416
  store.updateHistory(newTab)
416
417
  }
417
418
 
418
- // Dangerous props that should not be accepted from IPC
419
- const dangerousTabProps = [
420
- 'execLinux',
421
- 'execMac',
422
- 'execWindows',
423
- 'execWindowsArgs',
424
- 'execMacArgs',
425
- 'execLinuxArgs',
426
- 'setEnv',
427
- 'runScripts',
428
- 'interactiveValues',
429
- 'triggers'
430
- ]
431
-
432
419
  Store.prototype.ipcOpenTab = function (parsed) {
433
420
  const safeTab = Object.fromEntries(
434
- Object.entries(parsed).filter(([key]) => !dangerousTabProps.includes(key))
421
+ Object.entries(parsed).filter(([key]) => !dangerousSessionFields.includes(key))
435
422
  )
436
423
  return window.store.addTab(safeTab)
437
424
  }
@@ -4,7 +4,8 @@
4
4
 
5
5
  import message from '../components/common/message'
6
6
  import {
7
- settingMap
7
+ settingMap,
8
+ settingPanelMobileBreakpoint
8
9
  } from '../common/constants'
9
10
  import getInitItem from '../common/init-setting-item'
10
11
  import deepCopy from 'json-deep-copy'
@@ -27,12 +28,28 @@ export default Store => {
27
28
  const {
28
29
  widgetInstances
29
30
  } = window.store
30
- const index = widgetInstances.findIndex(w => w.id === instanceId)
31
- if (index > -1) {
32
- widgetInstances.splice(index, 1)
31
+ // assigned, not spliced: an in-place mutation fires no subscriber, and the
32
+ // instance list, the sidebar badge and the detail panel all read this array
33
+ window.store.widgetInstances = widgetInstances.filter(w => w.id !== instanceId)
34
+ }
35
+
36
+ // Selecting a running instance in the widgets list shows its detail + log in
37
+ // the right column, the same way selecting a widget shows its run form.
38
+ Store.prototype.selectWidgetInstance = (instance) => {
39
+ const { store } = window
40
+ store.setSettingItem(instance)
41
+ // narrow panels drill into the right col content after picking an instance
42
+ if (window.store.innerWidth <= settingPanelMobileBreakpoint) {
43
+ window.store.settingMobileView = 'content'
33
44
  }
34
45
  }
35
46
 
47
+ // back to the widget form — used when the instance being viewed is gone
48
+ Store.prototype.closeWidgetInstance = () => {
49
+ const { store } = window
50
+ store.setSettingItem(getInitItem([], settingMap.widgets))
51
+ }
52
+
36
53
  Store.prototype.stopWidget = async (instanceId) => {
37
54
  const {
38
55
  store
@@ -102,10 +119,11 @@ export default Store => {
102
119
  widgetId: result.widgetId,
103
120
  serverInfo: result.serverInfo,
104
121
  config: item.config,
122
+ startedAt: Date.now(),
105
123
  autoRun: true,
106
124
  autoRunId: item.id
107
125
  }
108
- store.widgetInstances.push(instance)
126
+ store.widgetInstances = [...store.widgetInstances, instance]
109
127
  }
110
128
  } catch (err) {
111
129
  console.error(`Failed to autorun widget ${item.widgetId}:`, err)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "5.5.35",
3
+ "version": "5.5.66",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,42 +0,0 @@
1
- /**
2
- * zip/unzip remote files
3
- * should only support linux server
4
- */
5
-
6
- import { runCmd } from '../terminal/terminal-apis'
7
- import { getFolderFromFilePath } from '../sftp/file-read'
8
- import resolve from '../../common/resolve'
9
- import generate from '../../common/uid'
10
-
11
- const isRemote = true
12
- const temp = '/tmp'
13
-
14
- export async function zipCmd (pid, filePath) {
15
- // tar -czf bin.tar bin
16
- const id = generate()
17
- const { path, name } = getFolderFromFilePath(filePath, isRemote)
18
- const np = resolve(temp, `electerm-${id}.tar`)
19
- const cmd = `tar -C "${path}" -cf "${np}" "${name}"`
20
- await runCmd(pid, cmd)
21
- return np
22
- }
23
-
24
- export function unzipCmd (pid, from, to) {
25
- const cmd = `tar -xf "${from}" -C "${to}"`
26
- return runCmd(pid, cmd)
27
- }
28
-
29
- export async function rmCmd (pid, path) {
30
- const cmd = `rm -rf "${path}"`
31
- return runCmd(pid, cmd)
32
- }
33
-
34
- export async function mvCmd (pid, from, to) {
35
- const cmd = `mv "${from}" "${to}"`
36
- return runCmd(pid, cmd)
37
- }
38
-
39
- export async function mkdirCmd (pid, p) {
40
- const cmd = `mkdir "${p}"`
41
- return runCmd(pid, cmd)
42
- }