@electerm/electerm-react 5.5.35 → 5.5.56

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 (40) hide show
  1. package/client/common/bookmark-schemas.js +1 -0
  2. package/client/common/constants.js +1 -0
  3. package/client/common/parse-quick-connect.js +1 -0
  4. package/client/common/parse-widget-log.js +57 -0
  5. package/client/components/ai/agent-tool-call-card.jsx +7 -3
  6. package/client/components/ai/agent.js +176 -33
  7. package/client/components/ai/ai-auto-compress.js +119 -0
  8. package/client/components/ai/ai-chat-history-item.jsx +125 -40
  9. package/client/components/ai/ai-chat.jsx +67 -19
  10. package/client/components/ai/ai-compress.js +95 -0
  11. package/client/components/ai/ai-config.jsx +29 -16
  12. package/client/components/ai/ai-config.styl +20 -0
  13. package/client/components/ai/ai-context.js +1 -1
  14. package/client/components/ai/ai-output.jsx +1 -6
  15. package/client/components/ai/ai-presets.js +0 -15
  16. package/client/components/batch-op/batch-op-runner.jsx +1 -0
  17. package/client/components/bookmark-form/bookmark-schema.js +1 -0
  18. package/client/components/bookmark-form/common/ssh-agent.jsx +31 -21
  19. package/client/components/bookmark-form/config/ssh.js +1 -0
  20. package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
  21. package/client/components/file-transfer/transfer.jsx +3 -119
  22. package/client/components/main/main.jsx +1 -0
  23. package/client/components/session/session-control.jsx +35 -2
  24. package/client/components/session/session-control.styl +5 -0
  25. package/client/components/session/session.jsx +13 -0
  26. package/client/components/setting-panel/tab-widgets.jsx +27 -7
  27. package/client/components/sftp/file-item.jsx +0 -13
  28. package/client/components/terminal/mixins/term-touch.js +18 -0
  29. package/client/components/widgets/widget-control.jsx +4 -2
  30. package/client/components/widgets/widget-instance-detail.jsx +370 -0
  31. package/client/components/widgets/widget-instance.jsx +30 -13
  32. package/client/components/widgets/widget-instances.jsx +3 -1
  33. package/client/components/widgets/widgets-list.jsx +8 -0
  34. package/client/components/widgets/widgets.styl +100 -0
  35. package/client/store/common.js +81 -23
  36. package/client/store/init-state.js +7 -0
  37. package/client/store/load-data.js +1 -0
  38. package/client/store/widgets.js +23 -5
  39. package/package.json +1 -1
  40. package/client/components/file-transfer/zip.js +0 -42
@@ -9,26 +9,36 @@ const e = window.translate
9
9
 
10
10
  export default function SshAgent () {
11
11
  return (
12
- <FormItem
13
- {...formItemLayout}
14
- label={e('useSshAgent')}
15
- >
16
- <Space align='center'>
17
- <FormItem
18
- name='useSshAgent'
19
- valuePropName='checked'
20
- noStyle
21
- >
22
- <SwitchLabel />
23
- </FormItem>
24
- <FormItem
25
- name='sshAgent'
26
- noStyle
27
- >
28
- <Input placeholder={e('SSH Agent Path')} />
29
- </FormItem>
30
- <HelpIcon link='https://github.com/electerm/electerm/wiki/ssh-agent' />
31
- </Space>
32
- </FormItem>
12
+ <>
13
+ <FormItem
14
+ {...formItemLayout}
15
+ label={e('useSshAgent')}
16
+ >
17
+ <Space align='center'>
18
+ <FormItem
19
+ name='useSshAgent'
20
+ valuePropName='checked'
21
+ noStyle
22
+ >
23
+ <SwitchLabel />
24
+ </FormItem>
25
+ <FormItem
26
+ name='sshAgent'
27
+ noStyle
28
+ >
29
+ <Input placeholder={e('SSH Agent Path')} />
30
+ </FormItem>
31
+ <HelpIcon link='https://github.com/electerm/electerm/wiki/ssh-agent' />
32
+ </Space>
33
+ </FormItem>
34
+ <FormItem
35
+ {...formItemLayout}
36
+ label={e('agentForward')}
37
+ name='agentForward'
38
+ valuePropName='checked'
39
+ >
40
+ <SwitchLabel />
41
+ </FormItem>
42
+ </>
33
43
  )
34
44
  }
@@ -22,6 +22,7 @@ const sshConfig = {
22
22
  connectionHoppings: [],
23
23
  useSshAgent: true,
24
24
  sshAgent: '',
25
+ agentForward: false,
25
26
  serverHostKey: [],
26
27
  cipher: [],
27
28
  compress: [],
@@ -6,6 +6,7 @@ const defaultValues = {
6
6
  enableSsh: true,
7
7
  enableSftp: true,
8
8
  useSshAgent: true,
9
+ agentForward: false,
9
10
  x11: false,
10
11
  term: 'xterm-256color',
11
12
  displayRaw: false,
@@ -12,13 +12,6 @@ import {
12
12
  import resolve from '../../common/resolve'
13
13
  import sanitizeFilename from '../../common/sanitize-filename'
14
14
  import { refsTransfers, refsStatic, refs } from '../common/ref'
15
- import {
16
- zipCmd,
17
- unzipCmd,
18
- rmCmd,
19
- mvCmd,
20
- mkdirCmd
21
- } from './zip'
22
15
  import './transfer.styl'
23
16
 
24
17
  const { assign } = Object
@@ -279,9 +272,7 @@ export default class TransportAction extends Component {
279
272
  typeFrom,
280
273
  toFile = {}
281
274
  } = transfer
282
- const toPath = transfer.zip
283
- ? transfer.toPath
284
- : this.newPath || transfer.toPath
275
+ const toPath = this.newPath || transfer.toPath
285
276
  const fromFile = transfer.fromFile || this.fromFile
286
277
  const fromMode = fromFile.mode
287
278
  const transferType = typeFrom === typeMap.local ? transferTypeMap.upload : transferTypeMap.download
@@ -408,12 +399,11 @@ export default class TransportAction extends Component {
408
399
  toPath
409
400
  } = this.props.transfer
410
401
  this.oldPath = toPath
411
- const { newPath, newName } = this.handleRename(toPath, typeTo === typeMap.remote)
402
+ const { newPath } = this.handleRename(toPath, typeTo === typeMap.remote)
412
403
  this.update({
413
404
  toPath: newPath
414
405
  })
415
406
  this.newPath = newPath
416
- this.newName = newName
417
407
  }
418
408
 
419
409
  const { typeFrom, typeTo } = this.props.transfer
@@ -423,120 +413,14 @@ export default class TransportAction extends Component {
423
413
  this.startTransfer()
424
414
  }
425
415
 
426
- zipTransferFolder = async () => {
427
- const {
428
- transfer
429
- } = this.props
430
- const {
431
- fromPath,
432
- typeFrom
433
- } = transfer
434
- const toPath = this.oldPath || transfer.toPath
435
- let p
436
- let isFromRemote
437
- if (typeFrom === typeMap.local) {
438
- isFromRemote = false
439
- p = await window.fs.zipFolder(fromPath)
440
- } else {
441
- isFromRemote = true
442
- const terminalId = refs.get('sftp-' + this.tabId)?.terminalId
443
- p = await zipCmd(terminalId, fromPath)
444
- }
445
- this.zipSrc = p
446
- const { name } = getFolderFromFilePath(p, isFromRemote)
447
- const { path } = getFolderFromFilePath(toPath, !isFromRemote)
448
- const nTo = resolve(path, name)
449
- this.zipPath = nTo
450
- const newTrans1 = {
451
- ...copy(transfer),
452
- toPath: nTo,
453
- fromPath: p
454
- }
455
- this.transferFile(newTrans1, this.unzipFile)
456
- }
457
-
458
- unzipFile = async () => {
459
- const { transfer } = this.props
460
- const {
461
- typeTo
462
- } = transfer
463
- const toPath = this.zipPath
464
- const fromPath = this.zipSrc
465
- const isToRemote = typeTo === typeMap.remote
466
- const {
467
- path,
468
- name,
469
- targetPath
470
- } = this.buildUnzipPath(transfer)
471
- const {
472
- newName,
473
- terminalId
474
- } = this
475
- if (isToRemote) {
476
- if (newName) {
477
- await mkdirCmd(terminalId, path)
478
- }
479
- await unzipCmd(terminalId, toPath, path)
480
- if (newName) {
481
- const mvFrom = resolve(path, name)
482
- const mvTo = resolve(targetPath, newName)
483
- await mvCmd(terminalId, mvFrom, mvTo)
484
- }
485
- } else {
486
- if (newName) {
487
- await window.fs.mkdir(path)
488
- }
489
- await window.fs.unzipFile(toPath, path)
490
- if (newName) {
491
- const mvFrom = resolve(path, name)
492
- const mvTo = resolve(targetPath, newName)
493
- await window.fs.mv(mvFrom, mvTo)
494
- }
495
- }
496
- await rmCmd(terminalId, !isToRemote ? fromPath : toPath)
497
- await window.fs.rmrf(!isToRemote ? toPath : fromPath)
498
- if (newName) {
499
- if (isToRemote) {
500
- await rmCmd(terminalId, path)
501
- } else {
502
- await window.fs.rmrf(path)
503
- }
504
- }
505
- this.onEnd()
506
- }
507
-
508
- buildUnzipPath = (transfer) => {
509
- const {
510
- typeTo
511
- } = transfer
512
- const isToRemote = typeTo === typeMap.remote
513
- const toPath = this.oldPath || transfer.toPath
514
- const {
515
- newName
516
- } = this
517
- const { path } = getFolderFromFilePath(toPath, isToRemote)
518
- const oldName = getFolderFromFilePath(toPath, isToRemote).name
519
- const np = newName
520
- ? resolve(path, 'temp-' + newName)
521
- : path
522
- return {
523
- targetPath: path,
524
- path: np,
525
- name: oldName
526
- }
527
- }
528
-
529
416
  startTransfer = async () => {
530
- const { fromFile = this.fromFile, zip } = this.props.transfer
417
+ const { fromFile = this.fromFile } = this.props.transfer
531
418
  if (!fromFile) {
532
419
  return
533
420
  }
534
421
  if (!fromFile.isDirectory) {
535
422
  return this.transferFile()
536
423
  }
537
- if (zip) {
538
- return this.zipTransferFolder()
539
- }
540
424
  if (!this.isFtp) {
541
425
  return this.transferFile()
542
426
  } else {
@@ -289,6 +289,7 @@ export default auto(function Index (props) {
289
289
  rightPanelTab,
290
290
  agentRunning: store.agentRunning,
291
291
  aiContextInfo: store.aiContextInfo,
292
+ aiAutoCompress: store.aiAutoCompress,
292
293
  currentChatSessionId: store.currentChatSessionId,
293
294
  showChatSessions: store.showChatSessions
294
295
  }
@@ -8,6 +8,7 @@ import {
8
8
  PaperClipOutlined,
9
9
  ApartmentOutlined,
10
10
  MoreOutlined,
11
+ MenuOutlined,
11
12
  ColumnWidthOutlined,
12
13
  LogoutOutlined,
13
14
  PlusOutlined,
@@ -39,6 +40,7 @@ export default function SessionControl (props) {
39
40
  keepaliveEnabled,
40
41
  broadcastInput,
41
42
  wrapDisabled,
43
+ pasteWhenContextMenu,
42
44
  onChangePane,
43
45
  toggleCheckSftpPathFollowSsh,
44
46
  onSshSftpSplitView,
@@ -47,6 +49,7 @@ export default function SessionControl (props) {
47
49
  toggleWrap,
48
50
  onFullscreen,
49
51
  onOpenSearch,
52
+ onOpenContextMenu,
50
53
  onExitGracefully,
51
54
  onZoomFontSize
52
55
  } = props
@@ -279,7 +282,35 @@ export default function SessionControl (props) {
279
282
  )
280
283
  }
281
284
 
282
- function renderTermControls () {
285
+ /**
286
+ * Touch devices have no right click, so the terminal's context menu needs a
287
+ * visible way in: this icon, next to the fullscreen icon on the desktop
288
+ * toolbar and next to the three-dot toggle in the mobile one (see below).
289
+ * Hidden when there is no menu to open — `pasteWhenContextMenu` turns both
290
+ * right click and the long press into a paste (see terminal.jsx), and it
291
+ * only ever acts on the terminal pane.
292
+ */
293
+ function renderContextMenuIcon (inMobileBar) {
294
+ if (
295
+ !isTouchDevice ||
296
+ pasteWhenContextMenu ||
297
+ tab.pane !== paneMap.terminal
298
+ ) {
299
+ return null
300
+ }
301
+ const cls = classnames(
302
+ 'iblock pointer spliter context-menu-control-icon',
303
+ inMobileBar ? 'mobile-context-menu-toggle' : 'mg1r icon-info'
304
+ )
305
+ return (
306
+ <MenuOutlined
307
+ className={cls}
308
+ onClick={onOpenContextMenu}
309
+ />
310
+ )
311
+ }
312
+
313
+ function renderTermControls (skipContextMenuIcon) {
283
314
  const { pane } = tab
284
315
  if (pane !== paneMap.terminal) {
285
316
  return null
@@ -287,6 +318,7 @@ export default function SessionControl (props) {
287
318
  return (
288
319
  <div className='fright term-controls'>
289
320
  {renderFontSizeIcons()}
321
+ {skipContextMenuIcon ? null : renderContextMenuIcon()}
290
322
  {renderFullscreenIcon()}
291
323
  {renderSearchIcon()}
292
324
  </div>
@@ -303,12 +335,13 @@ export default function SessionControl (props) {
303
335
  {renderBroadcastIcon()}
304
336
  {renderWrapIcon()}
305
337
  {renderExitGracefullyIcon()}
306
- {renderTermControls()}
338
+ {renderTermControls(true)}
307
339
  </div>
308
340
  )
309
341
  return (
310
342
  <div className='terminal-control mobile-session-control'>
311
343
  {renderPaneControl()}
344
+ {renderContextMenuIcon(true)}
312
345
  <Popover
313
346
  content={extraIcons}
314
347
  trigger='click'
@@ -14,6 +14,7 @@
14
14
  flex 1
15
15
  min-width 0
16
16
  .mobile-control-toggle
17
+ .mobile-context-menu-toggle
17
18
  font-size 16px
18
19
  line-height 32px
19
20
  color var(--text)
@@ -21,6 +22,10 @@
21
22
  &:hover
22
23
  color var(--text-light)
23
24
 
25
+ // touch-only terminal context menu icon, in the bar next to the toggle above
26
+ .mobile-context-menu-toggle
27
+ margin-right 10px
28
+
24
29
  // Popover content — icons laid out in a row, wrapping as needed
25
30
  .mobile-control-icons
26
31
  display flex
@@ -480,6 +480,17 @@ export default class SessionWrapper extends Component {
480
480
  refs.get('term-' + this.props.tab.id)?.toggleSearch()
481
481
  }
482
482
 
483
+ // touch-only: the toolbar menu icon opens the same context menu a right
484
+ // click (or a long press) does, anchored just below the icon itself
485
+ handleOpenContextMenu = (evt) => {
486
+ const term = refs.get('term-' + this.props.tab.id)
487
+ if (!term) {
488
+ return
489
+ }
490
+ const rect = evt.currentTarget.getBoundingClientRect()
491
+ term.openContextMenuAtPoint(rect.left, rect.bottom)
492
+ }
493
+
483
494
  handleZoomFontSize = (v) => {
484
495
  const term = refs.get('term-' + this.props.tab.id)
485
496
  if (!term?.term) {
@@ -625,6 +636,7 @@ export default class SessionWrapper extends Component {
625
636
  keepaliveEnabled={this.state.keepaliveEnabled}
626
637
  broadcastInput={this.state.broadcastInput}
627
638
  wrapDisabled={this.state.wrapDisabled}
639
+ pasteWhenContextMenu={!!this.props.config.pasteWhenContextMenu}
628
640
  onChangePane={(pane) => this.onChangePane(pane)}
629
641
  toggleCheckSftpPathFollowSsh={this.toggleCheckSftpPathFollowSsh}
630
642
  onSshSftpSplitView={this.handleSshSftpSplitView}
@@ -634,6 +646,7 @@ export default class SessionWrapper extends Component {
634
646
  onFullscreen={this.handleFullscreen}
635
647
  onZoomFontSize={this.handleZoomFontSize}
636
648
  onOpenSearch={this.handleOpenSearch}
649
+ onOpenContextMenu={this.handleOpenContextMenu}
637
650
  onExitGracefully={this.handleExitGracefully}
638
651
  />
639
652
  <Suspense fallback={null}>
@@ -1,11 +1,13 @@
1
1
  import SettingCol from './col'
2
2
  import WidgetControl from '../widgets/widget-control'
3
+ import WidgetInstanceDetail from '../widgets/widget-instance-detail'
3
4
  import WidgetList from '../widgets/widgets-list'
5
+ import { auto } from 'manate/react'
4
6
  import {
5
7
  settingMap
6
8
  } from '../../common/constants'
7
9
 
8
- export default function TabWidgets (props) {
10
+ export default auto(function TabWidgets (props) {
9
11
  const {
10
12
  settingTab
11
13
  } = props
@@ -15,8 +17,14 @@ export default function TabWidgets (props) {
15
17
  const {
16
18
  settingItem,
17
19
  listProps,
18
- formProps
20
+ formProps,
21
+ store
19
22
  } = props
23
+ // The right column shows whatever the list selected: a widget definition (run
24
+ // form) or a running instance (its detail + log). A definition has `info`, an
25
+ // instance has `widgetId` — and an instance that has been stopped stays the
26
+ // selected item on purpose, so its log is still readable after it dies.
27
+ const isInstance = !!(settingItem && settingItem.widgetId)
20
28
  return (
21
29
  <div
22
30
  className='setting-tabs-profile'
@@ -25,11 +33,23 @@ export default function TabWidgets (props) {
25
33
  <WidgetList
26
34
  {...listProps}
27
35
  />
28
- <WidgetControl
29
- {...formProps}
30
- key={settingItem.id}
31
- />
36
+ {
37
+ isInstance
38
+ ? (
39
+ <WidgetInstanceDetail
40
+ key={settingItem.id}
41
+ instance={settingItem}
42
+ store={store}
43
+ />
44
+ )
45
+ : (
46
+ <WidgetControl
47
+ {...formProps}
48
+ key={settingItem.id}
49
+ />
50
+ )
51
+ }
32
52
  </SettingCol>
33
53
  </div>
34
54
  )
35
- }
55
+ })
@@ -902,12 +902,6 @@ export default class FileSection extends React.Component {
902
902
  this.transfer()
903
903
  }
904
904
 
905
- zipAndTransfer = async () => {
906
- this.transfer(transfer => {
907
- transfer.zip = true
908
- })
909
- }
910
-
911
905
  newFile = () => {
912
906
  return this.newItem(false)
913
907
  }
@@ -1098,13 +1092,6 @@ export default class FileSection extends React.Component {
1098
1092
  icon: iconType,
1099
1093
  text: transferText
1100
1094
  })
1101
- // if (isDirectory && !this.props.isFtp) {
1102
- // res.push({
1103
- // func: 'zipAndTransfer',
1104
- // icon: 'FileZipOutlined',
1105
- // text: e('compressAndTransfer')
1106
- // })
1107
- // }
1108
1095
  }
1109
1096
  if (!isDirectory && isRealFile && isLocal) {
1110
1097
  res.push({
@@ -41,6 +41,10 @@
41
41
  * `stopPropagation()` is enough to veto xterm's scroll for the duration of
42
42
  * the drag. `preventDefault()` is not an option: React listens to
43
43
  * `touchmove` passively, so it is ignored (with a console warning).
44
+ *
45
+ * A long press is not the only way in: the toolbar shows a touch-only menu
46
+ * icon next to the three-dot toggle (see session-control.jsx), which opens the
47
+ * same menu through openContextMenuAtPoint() below.
44
48
  */
45
49
 
46
50
  const DEFAULT_WORD_SEPARATOR = './\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
@@ -505,6 +509,20 @@ export const touchMixin = {
505
509
  target.dispatchEvent(event)
506
510
  },
507
511
 
512
+ /**
513
+ * Opens the menu from the toolbar icon, which lives outside the antd
514
+ * dropdown's trigger element — so the event is dispatched on the terminal's
515
+ * own node and bubbles up to it. The icon's corner is passed in as the point
516
+ * to drop the menu from, rather than the finger that tapped it.
517
+ */
518
+ openContextMenuAtPoint (clientX, clientY) {
519
+ this.openContextMenuAt(
520
+ clientX,
521
+ clientY,
522
+ this.domRef?.current || this.term?.element
523
+ )
524
+ },
525
+
508
526
  cancelLongPress () {
509
527
  clearTimeout(this.longPressTimer)
510
528
  this.longPressTimer = null
@@ -46,9 +46,11 @@ export default function WidgetControl ({ formData, widgetInstancesLength }) {
46
46
  title: `${widget.info.name} (${result.instanceId})`,
47
47
  widgetId: result.widgetId,
48
48
  serverInfo: result.serverInfo,
49
- config
49
+ config,
50
+ startedAt: Date.now()
50
51
  }
51
- window.store.widgetInstances.push(instance)
52
+ // assigned, not pushed: see deleteWidgetInstance in store/widgets.js
53
+ window.store.widgetInstances = [...window.store.widgetInstances, instance]
52
54
  if (config.autoRun) {
53
55
  window.store.toggleAutoRunWidget(instance)
54
56
  }