@electerm/electerm-react 5.5.15 → 5.5.35

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 (64) hide show
  1. package/README.md +14 -4
  2. package/client/common/bookmark-schemas.js +3 -0
  3. package/client/common/constants.js +16 -0
  4. package/client/common/create-title.jsx +5 -1
  5. package/client/common/default-setting.js +4 -2
  6. package/client/common/parse-vv.js +407 -0
  7. package/client/common/theme-defaults.js +5 -2
  8. package/client/common/update-check.js +5 -4
  9. package/client/common/vv-to-tab.js +120 -0
  10. package/client/components/ai/agent-tool-call-card.jsx +19 -4
  11. package/client/components/ai/agent.js +33 -16
  12. package/client/components/ai/ai-chat-history-item.jsx +12 -48
  13. package/client/components/ai/ai-chat.jsx +82 -25
  14. package/client/components/ai/ai-config-props.js +12 -1
  15. package/client/components/ai/ai-config.jsx +14 -0
  16. package/client/components/ai/ai-context-indicator.jsx +83 -0
  17. package/client/components/ai/ai-context.js +272 -0
  18. package/client/components/ai/ai-output.jsx +4 -0
  19. package/client/components/ai/ai-presets.js +13 -2
  20. package/client/components/ai/ai.styl +105 -6
  21. package/client/components/ai/copy-icon.jsx +31 -0
  22. package/client/components/bg/css-overwrite.jsx +16 -1
  23. package/client/components/bookmark-form/common/fields.jsx +3 -0
  24. package/client/components/bookmark-form/common/vv-file-field.jsx +138 -0
  25. package/client/components/bookmark-form/config/spice.js +22 -0
  26. package/client/components/footer/cmd-history.jsx +279 -65
  27. package/client/components/footer/cmd-history.styl +68 -1
  28. package/client/components/footer/footer-entry.jsx +46 -15
  29. package/client/components/icons/ai-icon.styl +4 -1
  30. package/client/components/layout/layout.jsx +12 -13
  31. package/client/components/main/main.jsx +23 -3
  32. package/client/components/main/term-fullscreen.styl +7 -0
  33. package/client/components/quick-commands/qm.styl +58 -0
  34. package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
  35. package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
  36. package/client/components/quick-commands/quick-commands-box.jsx +197 -82
  37. package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
  38. package/client/components/quick-commands/quick-commands-select.jsx +11 -0
  39. package/client/components/setting-panel/list.jsx +2 -2
  40. package/client/components/setting-panel/setting-common.jsx +2 -0
  41. package/client/components/side-panel-r/right-side-panel.styl +11 -1
  42. package/client/components/side-panel-r/side-panel-r.jsx +56 -13
  43. package/client/components/sidebar/bookmark-select.jsx +17 -3
  44. package/client/components/sidebar/history-item.jsx +20 -4
  45. package/client/components/sidebar/history.jsx +7 -0
  46. package/client/components/sidebar/index.jsx +13 -4
  47. package/client/components/sidebar/side-panel.jsx +8 -3
  48. package/client/components/sidebar/sidebar-panel.jsx +20 -7
  49. package/client/components/terminal/shortcut-bar.styl +14 -0
  50. package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
  51. package/client/components/terminal-info/terminal-info.styl +3 -0
  52. package/client/components/theme/terminal-theme-list.styl +3 -0
  53. package/client/components/theme/theme-ai-editor.jsx +2 -2
  54. package/client/components/theme/theme-form.jsx +2 -2
  55. package/client/components/theme/theme-list-item.jsx +59 -25
  56. package/client/components/tree-list/tree-list.jsx +31 -3
  57. package/client/css/mobile.styl +28 -0
  58. package/client/store/common.js +164 -32
  59. package/client/store/init-state.js +10 -1
  60. package/client/store/load-data.js +72 -0
  61. package/client/store/sidebar.js +5 -1
  62. package/client/store/store.js +15 -2
  63. package/client/store/watch.js +3 -1
  64. package/package.json +1 -1
@@ -37,6 +37,7 @@ export default auto(function Layout (props) {
37
37
  height,
38
38
  isMobile,
39
39
  pinnedQuickCommandBar,
40
+ quickCommandsInRightPanel,
40
41
  leftSidePanelWidth,
41
42
  leftSideBarWidth,
42
43
  pinned,
@@ -49,7 +50,14 @@ export default auto(function Layout (props) {
49
50
  shortcutBarKbOffset
50
51
  } = props.store
51
52
  const monitorHeight = isRemoteMonitorBarVisible(props.store) ? remoteMonitorBarHeight : 0
52
- const h = height - footerHeight - monitorHeight - (inActiveTerminal && pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0) + resizeTrigger
53
+ // a pin left on while the panel is docked in the right side panel reserves
54
+ // nothing: the footer box is not rendered then (quick-commands-box.jsx)
55
+ const qmPinnedHeight = inActiveTerminal &&
56
+ pinnedQuickCommandBar &&
57
+ !quickCommandsInRightPanel
58
+ ? quickCommandBoxHeight
59
+ : 0
60
+ const h = height - footerHeight - monitorHeight - qmPinnedHeight - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0) + resizeTrigger
53
61
  const l = pinned && !isMobile ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
54
62
  const r = rightPanelVisible && rightPanelPinned && !isMobile ? rightPanelWidth : 0
55
63
  return {
@@ -141,18 +149,9 @@ export default auto(function Layout (props) {
141
149
  const footerProps = {
142
150
  store
143
151
  }
144
- const qmProps = pick(store, [
145
- 'quickCommandTags',
146
- 'qmSortByFrequency',
147
- 'openQuickCommandBar',
148
- 'pinnedQuickCommandBar',
149
- 'qmSortByFrequency',
150
- 'inActiveTerminal',
151
- 'leftSidePanelWidth',
152
- 'leftSideBarWidth',
153
- 'openedSideBar',
154
- 'currentQuickCommands'
155
- ])
152
+ const qmProps = {
153
+ store
154
+ }
156
155
  const sessionsProps = {
157
156
  styles: styles.wrapStyles,
158
157
  sizes,
@@ -29,6 +29,8 @@ import { isAIDisabled } from '../../common/ai-feature'
29
29
  import { ConfigProvider } from 'antd'
30
30
  import { NotificationContainer } from '../common/notification'
31
31
  import RightSidePanel from '../side-panel-r/side-panel-r'
32
+ import CmdHistory from '../footer/cmd-history'
33
+ import QuickCommandsFooterBox from '../quick-commands/quick-commands-box'
32
34
  import ConnectionHoppingWarning from './connection-hopping-warnning'
33
35
  import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
34
36
  import LoadSshConfigs from '../ssh-config/load-ssh-configs'
@@ -94,6 +96,7 @@ export default auto(function Index (props) {
94
96
  store.checkForDbUpgrade()
95
97
  store.handleGetSerials()
96
98
  store.checkPendingDeepLink()
99
+ store.checkPendingVvFile()
97
100
  }, [])
98
101
 
99
102
  // Track the actual input modality rather than a static capability probe:
@@ -128,6 +131,7 @@ export default auto(function Index (props) {
128
131
  pinned,
129
132
  isSecondInstance,
130
133
  pinnedQuickCommandBar,
134
+ quickCommandsInRightPanel,
131
135
  installSrc,
132
136
  fileTransfers,
133
137
  uiThemeConfig,
@@ -148,9 +152,16 @@ export default auto(function Index (props) {
148
152
  'is-mac': isMac,
149
153
  'not-mac': !isMac,
150
154
  'is-win': isWin,
151
- pinned,
155
+ // The left sidebar dock is desktop-only (see sidebar/index.jsx). This
156
+ // class drives the docked-layout CSS — the footer's left offset and the
157
+ // sidebar panel's top padding — so it has to follow the same gate as the
158
+ // layout maths in layout.jsx, or a pin persisted from a desktop session
159
+ // would restyle the mobile layout.
160
+ pinned: pinned && !store.isMobile,
152
161
  'not-win': !isWin,
153
- 'qm-pinned': pinnedQuickCommandBar,
162
+ // the pinned quick-command box only shapes the layout while it is actually
163
+ // in the footer — docked in the right panel it renders there instead
164
+ 'qm-pinned': pinnedQuickCommandBar && !quickCommandsInRightPanel,
154
165
  fullscreen,
155
166
  // terminal fullscreen keeps the footer visible (rdp/vnc/spice fullscreen
156
167
  // does not — the footer would be an empty bar there)
@@ -226,6 +237,7 @@ export default auto(function Index (props) {
226
237
  transferHistory: copiedHistory,
227
238
  upgradeInfo,
228
239
  pinned,
240
+ isMobile: store.isMobile,
229
241
  leftSideBarIcons: config.leftSideBarIcons,
230
242
  widgetInstancesLength: widgetInstances.length
231
243
  }
@@ -253,7 +265,8 @@ export default auto(function Index (props) {
253
265
  rightPanelPinned: store.rightPanelPinned,
254
266
  rightPanelWidth: store.rightPanelWidth,
255
267
  title: rightPanelTitle,
256
- rightPanelTab
268
+ rightPanelTab,
269
+ isMobile: store.isMobile
257
270
  }
258
271
  const sshConfigProps = {
259
272
  ...pick(store, [
@@ -275,6 +288,7 @@ export default auto(function Index (props) {
275
288
  showAIConfig: store.showAIConfig,
276
289
  rightPanelTab,
277
290
  agentRunning: store.agentRunning,
291
+ aiContextInfo: store.aiContextInfo,
278
292
  currentChatSessionId: store.currentChatSessionId,
279
293
  showChatSessions: store.showChatSessions
280
294
  }
@@ -351,6 +365,12 @@ export default auto(function Index (props) {
351
365
  <RightSidePanel {...rightPanelProps}>
352
366
  {!isAIDisabled() && <AIChat {...aiChatProps} />}
353
367
  <TerminalInfo key={store.activeTabId} store={store} {...deepCopy(store.terminalInfoProps)} />
368
+ {/* the cmd history panel has two homes; the footer popover is the
369
+ other one, and both are the same component (see cmd-history.jsx) */}
370
+ <CmdHistory store={store} inline />
371
+ {/* same for the quick command panel: the footer box is the other home
372
+ (see quick-commands-box.jsx) */}
373
+ <QuickCommandsFooterBox store={store} inline />
354
374
  </RightSidePanel>
355
375
  <SshConfigLoadNotify {...sshConfigProps} />
356
376
  <LoadSshConfigs
@@ -37,6 +37,13 @@
37
37
  right 10px !important
38
38
  bottom 10px !important
39
39
 
40
+ // RDP/VNC/Spice fullscreen has no footer (it is in the hide list above), so
41
+ // the right panel has nothing to clear and may use the full height. When the
42
+ // footer IS kept, see .fullscreen.fs-with-footer below.
43
+ .fullscreen:not(.fs-with-footer)
44
+ .right-side-panel
45
+ bottom 0
46
+
40
47
  // Terminal fullscreen keeps the footer (and the quick-command popup above it)
41
48
  // visible; the session shrinks to sit on top of the footer. RDP/VNC/Spice
42
49
  // fullscreen stays footer-less — inActiveTerminal is false for those tabs, so
@@ -2,6 +2,58 @@
2
2
  max-height calc(100vh - 100px)
3
3
  overflow-y scroll
4
4
  padding 20px 0 10px 0
5
+
6
+ // Docked in the right side panel the panel *is* the container, so the quick
7
+ // commands fill it instead of sizing themselves like the footer box. The parent
8
+ // (.right-side-panel-content) is an absolutely positioned box with top and
9
+ // bottom set, so 100% resolves to a real height and the list scrolls inside it
10
+ // rather than scrolling the panel.
11
+ .qm-panel-in-right-panel
12
+ display flex
13
+ flex-direction column
14
+ width 100%
15
+ height 100%
16
+ > .pd2
17
+ display flex
18
+ flex-direction column
19
+ height 100%
20
+ min-height 0
21
+ width 100%
22
+ .qm-flex
23
+ flex-shrink 0
24
+ // the footer box is as wide as the footer; the right panel is not, and the
25
+ // search + label + sort + actions row does not fit in one line there
26
+ flex-wrap wrap
27
+ // ...so give the search a line of its own and let it fill it, instead of
28
+ // sitting at its footer width with a gap beside it. `flex-basis: 100%` is what
29
+ // forces the wrap; the controls then share the line below.
30
+ .qm-search-input
31
+ flex 1 1 100%
32
+ max-width none
33
+ .qm-list-wrap
34
+ flex 1
35
+ min-height 0
36
+ max-height none
37
+ overflow-y auto
38
+
39
+ .qm-panel-actions
40
+ display flex
41
+ align-items center
42
+ gap 6px
43
+
44
+ .qm-move-icon
45
+ font-size 14px
46
+ padding 4px
47
+ cursor pointer
48
+ &:hover
49
+ color var(--primary)
50
+
51
+ // The footer Q stays lit while the panel is docked in the right panel, same
52
+ // contract as the info, AI and cmd history icons
53
+ .quick-command-trigger
54
+ &.active
55
+ color var(--success)
56
+
5
57
  .qm-wrap-tooltip
6
58
  background var(--main)
7
59
  position absolute
@@ -80,6 +132,12 @@
80
132
  width 100%
81
133
  .qm-label-select
82
134
  width 100%
135
+ // The docked panel's search claims a whole row with `flex-basis: 100%`, which
136
+ // is a width only while `.qm-flex` is a row. Here it is a column, so the same
137
+ // basis would be 100% of the *height* and would push the controls off the
138
+ // panel. Same specificity as the rule above it, declared later, so it wins.
139
+ .qm-panel-in-right-panel .qm-search-input
140
+ flex none
83
141
 
84
142
  // On mobile, stack the quick-command row (name / delay / command / remove)
85
143
  // vertically instead of side by side — the horizontal Space.Compact layout
@@ -0,0 +1,255 @@
1
+ /**
2
+ * AI-powered quick command generator.
3
+ *
4
+ * This is the "create with AI" tab of the quick command form: describe the
5
+ * quick command in natural language, the configured AI provider returns the
6
+ * quick command (name + sub commands in JSON), and the result is written back
7
+ * into the very same form through `onChange`, so the user reviews/edits it in
8
+ * the manual tab before saving.
9
+ */
10
+ import { useState, useEffect } from 'react'
11
+ import { Button, Input, Space, Alert } from 'antd'
12
+ import {
13
+ RobotOutlined,
14
+ LoadingOutlined
15
+ } from '@ant-design/icons'
16
+ import message from '../common/message'
17
+ import { appendMandatoryGuardrails } from '../ai/ai-guardrails'
18
+ import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
19
+ import HelpIcon from '../common/help-icon'
20
+ import { getItem, setItem } from '../../common/safe-local-storage'
21
+ import templates from './templates'
22
+ import './quick-command-ai-editor.styl'
23
+
24
+ const STORAGE_KEY_DESC = 'ai_quick_command_description'
25
+ const STORAGE_KEY_HISTORY = 'ai_quick_command_history'
26
+ const EVENT_NAME_HISTORY = 'ai-quick-command-history-update'
27
+ const defaultDelay = 100
28
+ const maxDelay = 65535
29
+ const maxNameLength = 60
30
+ const { TextArea } = Input
31
+ const e = window.translate
32
+
33
+ const wiki = 'https://github.com/electerm/electerm/wiki/Create-quick-command-by-AI'
34
+
35
+ /**
36
+ * build the prompt that asks the AI for one quick command definition
37
+ * @param {string} description
38
+ * @return {string}
39
+ */
40
+ export function buildPrompt (description) {
41
+ const lang = window.store.config.languageAI || window.store.getLangName()
42
+ const templateList = templates.map(t => `{{${t}}}`).join(', ')
43
+ return `You are an electerm quick command generator. Based on the user's natural language description, generate ONE quick command definition in JSON format.
44
+
45
+ The quick command JSON shape:
46
+ {
47
+ "name": "string (required) - short name of the quick command",
48
+ "commands": [
49
+ {
50
+ "name": "string - short name of this step",
51
+ "command": "string (required) - the exact shell command to send to the terminal",
52
+ "delay": "number - milliseconds to wait before sending this step, 1-${maxDelay}, default ${defaultDelay}"
53
+ }
54
+ ],
55
+ "inputOnly": "boolean - true means only type the command into the terminal without pressing enter, default false",
56
+ "labels": "array of strings - optional tags used to group quick commands"
57
+ }
58
+
59
+ Important rules:
60
+ 1. Respond with ONE quick command object, never an array of quick commands
61
+ 2. Split a multi step operation into several "commands" items, one shell command per item, and give a later step a longer "delay" when the step before it takes time
62
+ 3. "command" must be a plain shell command, no markdown, no code fence, no trailing newline
63
+ 4. Keep "name" short (max ${maxNameLength} chars) and write it in ${lang} language, labels included
64
+ 5. You may use the built in templates ${templateList} inside a command when the user asks for clipboard content, current time or current date
65
+ 6. Never invent hosts, paths, ports or credentials the user did not mention
66
+ 7. Respond ONLY with valid JSON, no markdown formatting and no extra explanation
67
+
68
+ User description: ${description}
69
+
70
+ Generate the quick command JSON:`
71
+ }
72
+
73
+ function stripFence (str) {
74
+ let s = str.trim()
75
+ if (s.startsWith('```json')) {
76
+ s = s.slice(7)
77
+ } else if (s.startsWith('```')) {
78
+ s = s.slice(3)
79
+ }
80
+ if (s.endsWith('```')) {
81
+ s = s.slice(0, -3)
82
+ }
83
+ return s.trim()
84
+ }
85
+
86
+ /**
87
+ * Normalize whatever the model returned into a quick command definition the
88
+ * quick command form can consume. Tolerates markdown fences, prose around the
89
+ * JSON, an array payload and string commands.
90
+ * @param {string|object} raw
91
+ * @return {object|null} {name, commands, inputOnly, labels}
92
+ */
93
+ export function normalizeQuickCommand (raw) {
94
+ if (!raw) {
95
+ return null
96
+ }
97
+ let data = raw
98
+ if (typeof data === 'string') {
99
+ const txt = stripFence(data)
100
+ try {
101
+ data = JSON.parse(txt)
102
+ } catch (err) {
103
+ // the model sometimes wraps the JSON in explanation text
104
+ const match = txt.match(/[{[][\s\S]*[}\]]/)
105
+ if (!match) {
106
+ return null
107
+ }
108
+ try {
109
+ data = JSON.parse(match[0])
110
+ } catch (err2) {
111
+ return null
112
+ }
113
+ }
114
+ }
115
+ if (Array.isArray(data)) {
116
+ data = data[0]
117
+ }
118
+ if (!data || typeof data !== 'object') {
119
+ return null
120
+ }
121
+ const rawCommands = Array.isArray(data.commands)
122
+ ? data.commands
123
+ : (data.command ? [data] : [])
124
+ const commands = rawCommands.map(c => {
125
+ if (typeof c === 'string') {
126
+ return {
127
+ name: '',
128
+ command: c,
129
+ delay: defaultDelay
130
+ }
131
+ }
132
+ const delay = Number(c.delay)
133
+ return {
134
+ name: (c.name || '').toString(),
135
+ command: (c.command || c.cmd || '').toString(),
136
+ delay: delay >= 1 ? Math.min(Math.round(delay), maxDelay) : defaultDelay
137
+ }
138
+ }).filter(c => c.command.trim())
139
+ if (!commands.length) {
140
+ return null
141
+ }
142
+ const name = (data.name || data.title || '').toString().trim()
143
+ return {
144
+ name: name || commands[0].command.slice(0, maxNameLength),
145
+ commands,
146
+ inputOnly: !!data.inputOnly,
147
+ labels: Array.isArray(data.labels)
148
+ ? data.labels.map(l => l.toString()).filter(Boolean)
149
+ : []
150
+ }
151
+ }
152
+
153
+ export default function QuickCommandAiEditor (props) {
154
+ const { onChange } = props
155
+ const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
156
+ const [loading, setLoading] = useState(false)
157
+ const [preview, setPreview] = useState('')
158
+
159
+ useEffect(() => {
160
+ setItem(STORAGE_KEY_DESC, description)
161
+ }, [description])
162
+
163
+ async function handleGenerate () {
164
+ if (window.store.aiConfigMissing()) {
165
+ window.store.toggleAIConfig()
166
+ return
167
+ }
168
+ if (!description.trim()) {
169
+ return message.warning(e('description') + ' ' + e('required'))
170
+ }
171
+ setLoading(true)
172
+ try {
173
+ const config = window.store.config
174
+ const aiResponse = await window.pre.runGlobalAsync(
175
+ 'AIchat',
176
+ buildPrompt(description),
177
+ config.modelAI,
178
+ appendMandatoryGuardrails('You are a helpful assistant that generates electerm quick commands.'),
179
+ config.baseURLAI,
180
+ config.apiPathAI,
181
+ config.apiKeyAI,
182
+ config.proxyAI,
183
+ false, // disable streaming, we need the whole JSON at once
184
+ config.authHeaderNameAI
185
+ )
186
+ if (aiResponse && aiResponse.error) {
187
+ throw new Error(aiResponse.error)
188
+ }
189
+ const data = normalizeQuickCommand(aiResponse && aiResponse.response)
190
+ if (!data) {
191
+ throw new Error('no quick command found in AI response')
192
+ }
193
+ setPreview(JSON.stringify(data, null, 2))
194
+ addHistoryItem(STORAGE_KEY_HISTORY, description, EVENT_NAME_HISTORY)
195
+ onChange(data)
196
+ message.success(e('Done'))
197
+ } catch (err) {
198
+ console.error('AI quick command generation error:', err)
199
+ message.error('Can not generate quick command from AI response: ' + err.message)
200
+ } finally {
201
+ setLoading(false)
202
+ }
203
+ }
204
+
205
+ const textAreaProps = {
206
+ value: description,
207
+ onChange: ev => setDescription(ev.target.value),
208
+ placeholder: e('description'),
209
+ autoSize: { minRows: 4, maxRows: 8 },
210
+ disabled: loading
211
+ }
212
+
213
+ const generateBtnProps = {
214
+ type: 'primary',
215
+ onClick: handleGenerate,
216
+ disabled: !description.trim(),
217
+ icon: loading ? <LoadingOutlined /> : <RobotOutlined />,
218
+ loading
219
+ }
220
+
221
+ return (
222
+ <div className='qm-ai-editor'>
223
+ <div className='pd1b'>
224
+ <Alert
225
+ type='info'
226
+ showIcon
227
+ title={e('aiSecurityNotice')}
228
+ />
229
+ </div>
230
+ <div className='pd1b'>
231
+ <TextArea {...textAreaProps} />
232
+ </div>
233
+ <AiHistory
234
+ storageKey={STORAGE_KEY_HISTORY}
235
+ eventName={EVENT_NAME_HISTORY}
236
+ onSelect={setDescription}
237
+ />
238
+ <div className='pd1t'>
239
+ <Space>
240
+ <Button {...generateBtnProps}>
241
+ {e('submit')}
242
+ </Button>
243
+ <HelpIcon link={wiki} />
244
+ </Space>
245
+ </div>
246
+ {
247
+ !!preview && (
248
+ <div className='pd1t'>
249
+ <pre className='qm-ai-preview'>{preview}</pre>
250
+ </div>
251
+ )
252
+ }
253
+ </div>
254
+ )
255
+ }
@@ -0,0 +1,13 @@
1
+ // "create with AI" tab of the quick command form
2
+ .qm-ai-editor
3
+ // the generated quick command, kept visible for reference after it has been
4
+ // written back into the form
5
+ .qm-ai-preview
6
+ padding 12px
7
+ margin 0
8
+ max-height 300px
9
+ overflow auto
10
+ font-size 12px
11
+ font-family monospace
12
+ white-space pre-wrap
13
+ word-break break-word