@electerm/electerm-react 3.15.159 → 3.15.161

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.
@@ -1,11 +1,12 @@
1
1
  import { agentTools, executeToolCall } from './agent-tools'
2
+ import { appendMandatoryGuardrails } from './ai-guardrails'
2
3
 
3
4
  const MAX_ITERATIONS = 150
4
5
 
5
6
  function buildAgentSystemPrompt (config) {
6
7
  const lang = config.languageAI || window.store.getLangName()
7
8
  const baseRole = config.roleAI || 'You are a helpful assistant.'
8
- return `${baseRole}
9
+ return appendMandatoryGuardrails(`${baseRole}
9
10
 
10
11
  You are operating inside electerm, a terminal/SSH client. You have access to tools that let you:
11
12
  - Run commands in terminal tabs and read their output
@@ -21,7 +22,7 @@ Prefer using the active terminal unless the user specifies otherwise.
21
22
  For SSH connections, prefer using open_tab to connect directly, or create a bookmark with add_bookmark and open it with open_bookmark if the user wants to save the connection.
22
23
  For file transfers, use the sftp_upload and sftp_download tools. The tab must be an SSH/FTP connection with SFTP initialized.
23
24
 
24
- Reply in ${lang} language.`
25
+ Reply in ${lang} language.`)
25
26
  }
26
27
 
27
28
  function updateChatEntry (chatEntry, updates) {
@@ -3,6 +3,7 @@ import AIOutput from './ai-output'
3
3
  import AIStopIcon from './ai-stop-icon'
4
4
  import AgentToolCallCard from './agent-tool-call-card'
5
5
  import { runAgentLoop } from './agent'
6
+ import { appendMandatoryGuardrails } from './ai-guardrails'
6
7
  import {
7
8
  Alert,
8
9
  Tooltip
@@ -42,7 +43,7 @@ export default function AIChatHistoryItem ({ item }) {
42
43
 
43
44
  function buildRole () {
44
45
  const lang = languageAI || window.store.getLangName()
45
- return roleAI + `;用[${lang}]回复`
46
+ return appendMandatoryGuardrails(roleAI + `;用[${lang}]回复`)
46
47
  }
47
48
 
48
49
  const conversationMessages = useMemo(() => {
@@ -253,6 +253,9 @@ export default function AIChat (props) {
253
253
  {renderSendIcon()}
254
254
  </Flex>
255
255
  </Flex>
256
+ {window.et.AIDisclamer && (
257
+ <div className='ai-disclamer mg1t'>{window.et.AIDisclamer}</div>
258
+ )}
256
259
  </Flex>
257
260
  )
258
261
  }
@@ -18,6 +18,7 @@ import Password from '../common/password'
18
18
  import AiHistory, { addHistoryItem } from './ai-history'
19
19
  import message from '../common/message'
20
20
  import { getAIPresets } from './ai-config-props'
21
+ import { appendMandatoryGuardrails } from './ai-guardrails'
21
22
 
22
23
  const STORAGE_KEY_CONFIG = 'ai_config_history'
23
24
  const EVENT_NAME_CONFIG = 'ai-config-history-update'
@@ -74,7 +75,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
74
75
  'AIchat',
75
76
  'Hi',
76
77
  values.modelAI,
77
- values.roleAI,
78
+ appendMandatoryGuardrails(values.roleAI),
78
79
  values.baseURLAI,
79
80
  values.apiPathAI,
80
81
  values.apiKeyAI,
@@ -164,7 +165,14 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
164
165
  <Link to={aiConfigWikiLink}>WIKI: {aiConfigWikiLink}</Link>
165
166
  }
166
167
  type='info'
167
- className='mg2y'
168
+ className='mg2t mg1b'
169
+ />
170
+ <Alert
171
+ title={
172
+ window.translate('aiWarn')
173
+ }
174
+ type='warning'
175
+ className='mg2b'
168
176
  />
169
177
  <div className='mg1b alignright'>
170
178
  {renderPresetMenu()}
@@ -0,0 +1,10 @@
1
+ // Append mandatory guardrails (if configured via window.et.mandatoryGuardrails)
2
+ // to the AI system role as an extra layer of protection. This is typically set
3
+ // by enterprise deployments to enforce policies on every AI request.
4
+ export function appendMandatoryGuardrails (systemContent) {
5
+ const guardrails = window.et && window.et.mandatoryGuardrails
6
+ if (!guardrails) {
7
+ return systemContent
8
+ }
9
+ return `${systemContent}\n\n${guardrails}`
10
+ }
@@ -2,12 +2,19 @@ import { useRef, useEffect } from 'react'
2
2
  import ReactMarkdown from 'react-markdown'
3
3
  import { copy } from '../../common/clipboard'
4
4
  import Link from '../common/external-link'
5
- import { Tag } from 'antd'
6
- import { CopyOutlined, PlayCircleOutlined } from '@ant-design/icons'
5
+ import { Tag, Popconfirm, Button, Alert } from 'antd'
6
+ import {
7
+ CopyOutlined,
8
+ PlayCircleOutlined,
9
+ FlagOutlined,
10
+ FlagFilled
11
+ } from '@ant-design/icons'
7
12
  import getBrand from './get-brand'
8
13
 
9
14
  const e = window.translate
10
15
 
16
+ const enableAIFlag = !!(window.et && window.et.enableAIFlag)
17
+
11
18
  export default function AIOutput ({ item }) {
12
19
  const outputRef = useRef(null)
13
20
  const {
@@ -23,7 +30,7 @@ export default function AIOutput ({ item }) {
23
30
  }
24
31
  }, [response])
25
32
 
26
- if (!response) {
33
+ if (!response && !(item.flagged && enableAIFlag)) {
27
34
  return null
28
35
  }
29
36
 
@@ -89,6 +96,38 @@ export default function AIOutput ({ item }) {
89
96
  )
90
97
  }
91
98
 
99
+ function handleToggleFlag () {
100
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
101
+ if (index === -1) {
102
+ return
103
+ }
104
+ window.store.aiChatHistory[index].flagged = !window.store.aiChatHistory[index].flagged
105
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
106
+ }
107
+
108
+ function renderFlag () {
109
+ if (!enableAIFlag) {
110
+ return null
111
+ }
112
+ return (
113
+ <div className={'ai-stream-output-flag' + (item.flagged ? ' is-flagged' : '')}>
114
+ <Popconfirm
115
+ title={item.flagged ? 'Remove the harmful-info flag?' : 'Flag this as harmful info?'}
116
+ okText='Confirm'
117
+ cancelText='Cancel'
118
+ onConfirm={handleToggleFlag}
119
+ >
120
+ <Button
121
+ size='small'
122
+ type='text'
123
+ icon={item.flagged ? <FlagFilled /> : <FlagOutlined />}
124
+ onClick={(evt) => evt.stopPropagation()}
125
+ />
126
+ </Popconfirm>
127
+ </div>
128
+ )
129
+ }
130
+
92
131
  function renderBrand () {
93
132
  if (!brand) {
94
133
  return null
@@ -113,9 +152,16 @@ export default function AIOutput ({ item }) {
113
152
 
114
153
  return (
115
154
  <div className='ai-stream-output' ref={outputRef}>
155
+ {renderFlag()}
116
156
  <div className='pd1'>
117
- {renderBrand()}
118
- <ReactMarkdown {...mdProps} />
157
+ {item.flagged && enableAIFlag
158
+ ? <Alert type='warning' message='user flagged as harmful info' />
159
+ : (
160
+ <>
161
+ {renderBrand()}
162
+ <ReactMarkdown {...mdProps} />
163
+ </>
164
+ )}
119
165
  </div>
120
166
  </div>
121
167
  )
@@ -33,8 +33,17 @@
33
33
  // display block
34
34
 
35
35
  .ai-stream-output
36
+ position relative
36
37
  max-height 400px
37
38
  overflow-y auto
39
+ &::-webkit-scrollbar-thumb
40
+ background var(--main-dark)
41
+ .ai-stream-output-flag
42
+ position absolute
43
+ top 13px
44
+ right 10px
45
+ z-index 10
46
+
38
47
 
39
48
  .ai-chat-input
40
49
  position relative
@@ -52,6 +61,9 @@
52
61
  z-index 1
53
62
  font-size 16px
54
63
 
64
+ .ai-disclamer
65
+ font-size 12px
66
+
55
67
  .code-block
56
68
  width 100%
57
69
  pre
@@ -69,6 +81,7 @@
69
81
 
70
82
  .ai-chat-sessions-wrap
71
83
  width 100%
84
+ height 100%
72
85
  overflow-y auto
73
86
  overflow-x hidden
74
87
  padding 4px 0
@@ -18,6 +18,7 @@ import {
18
18
  import SimpleEditor from '../text-editor/simple-editor'
19
19
  import { copy } from '../../common/clipboard'
20
20
  import download from '../../common/download'
21
+ import { appendMandatoryGuardrails } from '../ai/ai-guardrails'
21
22
  import AICategorySelect from './common/ai-category-select.jsx'
22
23
  import HelpIcon from '../common/help-icon'
23
24
  import Modal from '../common/modal.jsx'
@@ -77,7 +78,7 @@ export default function AIBookmarkForm (props) {
77
78
  'AIchat',
78
79
  prompt,
79
80
  config.modelAI,
80
- 'You are a helpful assistant that generates bookmark configurations in JSON format.',
81
+ appendMandatoryGuardrails('You are a helpful assistant that generates bookmark configurations in JSON format.'),
81
82
  config.baseURLAI,
82
83
  config.apiPathAI,
83
84
  config.apiKeyAI,
@@ -1,4 +1,4 @@
1
- import React, { useRef, useEffect } from 'react'
1
+ import React, { useRef, useEffect, useState } from 'react'
2
2
  import {
3
3
  ArrowUpOutlined,
4
4
  EyeInvisibleFilled,
@@ -7,11 +7,13 @@ import {
7
7
  ArrowRightOutlined,
8
8
  LoadingOutlined,
9
9
  HomeOutlined,
10
- PlusOutlined
10
+ PlusOutlined,
11
+ MenuOutlined
11
12
  } from '@ant-design/icons'
12
13
  import {
13
14
  Input,
14
- Tooltip
15
+ Tooltip,
16
+ Popover
15
17
  } from 'antd'
16
18
  import {
17
19
  typeMap
@@ -22,10 +24,11 @@ import KeywordFilter from './keyword-filter'
22
24
 
23
25
  const e = window.translate
24
26
 
25
- function renderAddonBefore (props, realPath) {
27
+ function AddressPrefixIcons (props) {
26
28
  const {
27
29
  type,
28
- host
30
+ host,
31
+ realPath
29
32
  } = props
30
33
  const isShow = props[`${type}ShowHiddenFile`]
31
34
  const title = `${isShow ? e('hide') : e('show')} ${e('hfd')}`
@@ -75,6 +78,37 @@ function renderAddonBefore (props, realPath) {
75
78
  )
76
79
  }
77
80
 
81
+ function MobileAddressPrefix (props) {
82
+ const [open, setOpen] = useState(false)
83
+ const content = (
84
+ <div className='sftp-addr-menu-content'>
85
+ <AddressPrefixIcons {...props} />
86
+ </div>
87
+ )
88
+ return (
89
+ <Popover
90
+ content={content}
91
+ trigger='click'
92
+ placement='bottomLeft'
93
+ open={open}
94
+ onOpenChange={setOpen}
95
+ >
96
+ <MenuOutlined className='sftp-addr-menu-icon' />
97
+ </Popover>
98
+ )
99
+ }
100
+
101
+ function renderAddonBefore (props, realPath) {
102
+ if (window.store.isMobile) {
103
+ return (
104
+ <MobileAddressPrefix {...props} realPath={realPath} />
105
+ )
106
+ }
107
+ return (
108
+ <AddressPrefixIcons {...props} realPath={realPath} />
109
+ )
110
+ }
111
+
78
112
  function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type, handleUploadFromBrowser) {
79
113
  const handleClick = (e) => {
80
114
  e.stopPropagation()
@@ -137,6 +137,23 @@ export default class FileListTable extends Component {
137
137
  ]
138
138
  }
139
139
 
140
+ /**
141
+ * In mobile mode, only show the file name to save horizontal space.
142
+ * Returns the effective properties used for rendering both the
143
+ * table header and individual file items.
144
+ */
145
+ getRenderProperties = () => {
146
+ if (window.store.isMobile) {
147
+ return [{
148
+ id: 'name',
149
+ max: 98,
150
+ min: 2,
151
+ size: 100
152
+ }]
153
+ }
154
+ return this.state.properties
155
+ }
156
+
140
157
  getPropsAll = () => {
141
158
  return [
142
159
  'name',
@@ -179,7 +196,7 @@ export default class FileListTable extends Component {
179
196
  onContextMenu: this.onContextMenu,
180
197
  onClickName: this.onClickName,
181
198
  onResize: this.onResize,
182
- properties: this.state.properties,
199
+ properties: this.getRenderProperties(),
183
200
  sortDirection: this.props.sortDirection,
184
201
  sortProp: this.props.sortProp,
185
202
  maxWidth: this.props.width
@@ -274,7 +291,7 @@ export default class FileListTable extends Component {
274
291
  const fileProps = {
275
292
  ...this.props.getFileProps(item, type),
276
293
  cls,
277
- properties: this.state.properties,
294
+ properties: this.getRenderProperties(),
278
295
  setClickFileId: this.setClickFileId
279
296
  }
280
297
  return (
@@ -1211,7 +1211,7 @@ export default class Sftp extends Component {
1211
1211
  renderSftpPanelTitle (type, username, host) {
1212
1212
  if (type === typeMap.remote) {
1213
1213
  return (
1214
- <div className='sftp-panel-title pd1t pd1b pd1x alignright'>
1214
+ <div className='sftp-panel-title pd1t pd1b pd1x alignright elli'>
1215
1215
  <ReloadOutlined
1216
1216
  className='mg1r pointer'
1217
1217
  onClick={this.handleReloadRemoteSftp}
@@ -185,3 +185,17 @@
185
185
  &:hover
186
186
  &.active
187
187
  color var(--success)
188
+
189
+ .sftp-addr-menu-icon
190
+ cursor pointer
191
+ font-size 14px
192
+ color var(--text)
193
+ &:hover
194
+ color var(--text-light)
195
+
196
+ .sftp-addr-menu-content
197
+ display flex
198
+ align-items center
199
+ padding 4px 0
200
+ flex-wrap wrap
201
+ gap 4px
@@ -2,6 +2,7 @@ import { Component } from 'manate/react/class-components'
2
2
  import { refsStatic, refs } from '../common/ref'
3
3
  import SuggestionItem from './cmd-item'
4
4
  import { aiSuggestionsCache } from '../../common/cache'
5
+ import { appendMandatoryGuardrails } from '../ai/ai-guardrails'
5
6
  import classnames from 'classnames'
6
7
  import {
7
8
  LoadingOutlined
@@ -67,7 +68,7 @@ export default class TerminalCmdSuggestions extends Component {
67
68
  'AIchat',
68
69
  prompt,
69
70
  config.modelAI,
70
- config.roleAI,
71
+ appendMandatoryGuardrails(config.roleAI),
71
72
  config.baseURLAI,
72
73
  config.apiPathAI,
73
74
  config.apiKeyAI,
@@ -132,3 +132,22 @@
132
132
  margin-inline-end 0
133
133
  &::before
134
134
  display none
135
+
136
+ // ---- 7. Terminal info panel button group: on narrow screens the connected
137
+ // button row in terminal info panel wraps, but antd styles it as a
138
+ // single horizontal strip. When the row wraps, the UI looks broken.
139
+ // Convert each button to a standalone pill so wrapping stays tidy.
140
+ .terminal-info-base
141
+ .ant-space-compact
142
+ display flex
143
+ flex-wrap wrap
144
+ gap 6px
145
+ .ant-btn
146
+ border-radius 3px
147
+ margin-right 0
148
+
149
+ // ---- 8. Upgrade panel: fixed 440px width overflows narrow mobile screens.
150
+ // Constrain it to the viewport minus the right offset (10px) and a
151
+ // matching left gap so it never extends past the screen edge.
152
+ .upgrade-panel
153
+ max-width calc(100vw - 20px)
@@ -4,6 +4,7 @@
4
4
 
5
5
  import handleError from '../common/error-handler'
6
6
  import Modal from '../components/common/modal'
7
+ import { appendMandatoryGuardrails } from '../components/ai/ai-guardrails'
7
8
  import { debounce, some, get, pickBy } from 'lodash-es'
8
9
  import {
9
10
  leftSidebarWidthKey,
@@ -106,7 +107,7 @@ export default Store => {
106
107
 
107
108
  Store.prototype.setLeftSidePanelWidth = function (v) {
108
109
  ls.setItem(leftSidebarWidthKey, v)
109
- window.store.leftSidebarWidth = v
110
+ window.store._leftSidebarWidth = v
110
111
  }
111
112
 
112
113
  Store.prototype.setAddPanelWidth = function (v) {
@@ -116,7 +117,7 @@ export default Store => {
116
117
 
117
118
  Store.prototype.setRightSidePanelWidth = function (v) {
118
119
  ls.setItem(rightSidebarWidthKey, v)
119
- window.store.rightPanelWidth = v
120
+ window.store._rightPanelWidth = v
120
121
  }
121
122
  Store.prototype.dismissDelKeyTip = function (v) {
122
123
  ls.setItem(dismissDelKeyTipLsKey, 'y')
@@ -322,7 +323,7 @@ export default Store => {
322
323
  const firstEntry = sessionEntries[0]
323
324
  const lang = firstEntry.languageAI || store.getLangName()
324
325
  const messages = [
325
- { role: 'system', content: firstEntry.roleAI + `;用[${lang}]回复` }
326
+ { role: 'system', content: appendMandatoryGuardrails(firstEntry.roleAI + `;用[${lang}]回复`) }
326
327
  ]
327
328
 
328
329
  // Start from the last compress entry to include its summary as context
@@ -123,7 +123,7 @@ export default () => {
123
123
  rightPanelVisible: false,
124
124
  rightPanelTab: 'info',
125
125
  rightPanelPinned: false,
126
- rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
126
+ _rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
127
127
  showAIConfigModal: false,
128
128
 
129
129
  // for settings related
@@ -164,7 +164,7 @@ export default () => {
164
164
 
165
165
  // sidebar
166
166
  openedSideBar: ls.getItem(openedSidebarKey) || '',
167
- leftSidebarWidth: parseInt(ls.getItem(leftSidebarWidthKey), 10) || 300,
167
+ _leftSidebarWidth: parseInt(ls.getItem(leftSidebarWidthKey), 10) || 300,
168
168
  addPanelWidth: parseInt(ls.getItem(addPanelWidthLsKey), 10) || 300,
169
169
  menuOpened: false,
170
170
  pinned: ls.getItem(sidebarPinnedKey) === 'true',
@@ -37,7 +37,8 @@ import {
37
37
  terminalSshConfigType,
38
38
  paneMap,
39
39
  staticNewItemTabs,
40
- isWin
40
+ isWin,
41
+ sidebarWidth
41
42
  } from '../common/constants'
42
43
  import getInitItem from '../common/init-setting-item'
43
44
  import createTitle from '../common/create-title'
@@ -58,6 +59,22 @@ class Store {
58
59
  return window.store.innerWidth
59
60
  }
60
61
 
62
+ get leftSidebarWidth () {
63
+ const { store } = window
64
+ if (store.isMobile) {
65
+ return Math.min(store.innerWidth - sidebarWidth, 500)
66
+ }
67
+ return store._leftSidebarWidth
68
+ }
69
+
70
+ get rightPanelWidth () {
71
+ const { store } = window
72
+ if (store.isMobile) {
73
+ return Math.min(store.innerWidth, 500)
74
+ }
75
+ return store._rightPanelWidth
76
+ }
77
+
61
78
  get config () {
62
79
  return deepCopy(window.store._config)
63
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.15.159",
3
+ "version": "3.15.161",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",