@electerm/electerm-react 5.5.15 → 5.5.25
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.
- package/README.md +12 -2
- package/client/common/constants.js +14 -0
- package/client/common/default-setting.js +2 -2
- package/client/common/parse-vv.js +313 -0
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/components/ai/ai-presets.js +13 -2
- package/client/components/bg/css-overwrite.jsx +16 -1
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +137 -0
- package/client/components/bookmark-form/config/spice.js +1 -0
- package/client/components/footer/footer-entry.jsx +39 -14
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +7 -1
- package/client/components/main/main.jsx +9 -2
- package/client/components/main/term-fullscreen.styl +7 -0
- package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
- package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
- package/client/components/quick-commands/quick-commands-box.jsx +23 -5
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +45 -9
- package/client/components/sidebar/bookmark-select.jsx +3 -1
- package/client/components/sidebar/index.jsx +13 -4
- package/client/components/sidebar/side-panel.jsx +8 -3
- package/client/components/sidebar/sidebar-panel.jsx +20 -7
- package/client/components/terminal/shortcut-bar.styl +14 -0
- package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
- package/client/components/terminal-info/terminal-info.styl +3 -0
- package/client/components/theme/terminal-theme-list.styl +3 -0
- package/client/components/theme/theme-ai-editor.jsx +2 -2
- package/client/components/theme/theme-form.jsx +2 -2
- package/client/components/theme/theme-list-item.jsx +59 -25
- package/client/css/mobile.styl +28 -0
- package/client/store/common.js +44 -0
- package/client/store/init-state.js +2 -1
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +9 -2
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ const {
|
|
|
24
24
|
|
|
25
25
|
export default auto(function FooterEntry (props) {
|
|
26
26
|
function handleInfoPanel () {
|
|
27
|
-
window.store.
|
|
27
|
+
window.store.toggleInfoPanel()
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function batchInput (cmd, selectedTabIds) {
|
|
@@ -106,11 +106,19 @@ export default auto(function FooterEntry (props) {
|
|
|
106
106
|
)
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function handleAIPanel () {
|
|
110
|
+
window.store.toggleAIPanel()
|
|
111
|
+
}
|
|
112
|
+
|
|
109
113
|
function renderAIIcon () {
|
|
114
|
+
const { rightPanelVisible, rightPanelTab } = props.store
|
|
115
|
+
// same contract as the info icon: lit while its panel is open
|
|
116
|
+
const active = rightPanelVisible && rightPanelTab === 'ai'
|
|
110
117
|
return (
|
|
111
118
|
<div className='terminal-footer-unit terminal-footer-ai'>
|
|
112
119
|
<AIIcon
|
|
113
|
-
onClick={
|
|
120
|
+
onClick={handleAIPanel}
|
|
121
|
+
className={active ? 'active' : ''}
|
|
114
122
|
/>
|
|
115
123
|
</div>
|
|
116
124
|
)
|
|
@@ -176,11 +184,15 @@ export default auto(function FooterEntry (props) {
|
|
|
176
184
|
if (loading) {
|
|
177
185
|
return null
|
|
178
186
|
}
|
|
187
|
+
const { rightPanelVisible, rightPanelTab } = props.store
|
|
188
|
+
// keep the icon lit while its panel is open so it reads as "click to close"
|
|
189
|
+
const active = rightPanelVisible && rightPanelTab === 'info'
|
|
179
190
|
return (
|
|
180
191
|
<div className='terminal-footer-unit terminal-footer-info'>
|
|
181
192
|
<BarChartOutlined
|
|
182
193
|
onClick={handleInfoPanel}
|
|
183
|
-
className='pointer font14 terminal-info-icon'
|
|
194
|
+
className={'pointer font14 terminal-info-icon' + (active ? ' active' : '')}
|
|
195
|
+
title={e('info')}
|
|
184
196
|
/>
|
|
185
197
|
</div>
|
|
186
198
|
)
|
|
@@ -202,7 +214,11 @@ export default auto(function FooterEntry (props) {
|
|
|
202
214
|
leftSidePanelWidth,
|
|
203
215
|
leftSideBarWidth,
|
|
204
216
|
openedSideBar,
|
|
205
|
-
inActiveTerminal
|
|
217
|
+
inActiveTerminal,
|
|
218
|
+
rightPanelVisible,
|
|
219
|
+
rightPanelPinned,
|
|
220
|
+
rightPanelWidth,
|
|
221
|
+
isMobile
|
|
206
222
|
} = props.store
|
|
207
223
|
const w = leftSideBarWidth + leftSidePanelWidth
|
|
208
224
|
// icon bar hidden: show a control on the left of the footer to bring the
|
|
@@ -217,16 +233,25 @@ export default auto(function FooterEntry (props) {
|
|
|
217
233
|
</div>
|
|
218
234
|
)
|
|
219
235
|
: null
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
// The footer mirrors the space layout.jsx reserves on both sides.
|
|
237
|
+
// Left: reserve while the panel is open OR pinned. A pinned left panel is
|
|
238
|
+
// always open, so this is a superset of the pin state — and it is what keeps
|
|
239
|
+
// the footer from painting over the panel, which spans bottom 0 in both
|
|
240
|
+
// states. Right: reserve only for a pinned panel, because the unpinned right
|
|
241
|
+
// panel is an overlay that stops above the footer and so never needs the
|
|
242
|
+
// space. Mobile reserves nothing on either side, matching layout.jsx, and the
|
|
243
|
+
// right panel has no pin control there at all.
|
|
244
|
+
const footerStyle = {}
|
|
245
|
+
if (openedSideBar && !isMobile) {
|
|
246
|
+
footerStyle.left = `${w}px`
|
|
247
|
+
}
|
|
248
|
+
if (rightPanelVisible && rightPanelPinned && !isMobile) {
|
|
249
|
+
footerStyle.right = `${rightPanelWidth}px`
|
|
250
|
+
}
|
|
251
|
+
const sideProps = {
|
|
252
|
+
className: 'main-footer',
|
|
253
|
+
style: footerStyle
|
|
254
|
+
}
|
|
230
255
|
if (
|
|
231
256
|
!inActiveTerminal
|
|
232
257
|
) {
|
|
@@ -151,7 +151,13 @@ export default auto(function Layout (props) {
|
|
|
151
151
|
'leftSidePanelWidth',
|
|
152
152
|
'leftSideBarWidth',
|
|
153
153
|
'openedSideBar',
|
|
154
|
-
'currentQuickCommands'
|
|
154
|
+
'currentQuickCommands',
|
|
155
|
+
// the quick-command popup mirrors the footer's horizontal extent, so it
|
|
156
|
+
// needs the same right-panel state the footer uses
|
|
157
|
+
'rightPanelVisible',
|
|
158
|
+
'rightPanelPinned',
|
|
159
|
+
'rightPanelWidth',
|
|
160
|
+
'isMobile'
|
|
155
161
|
])
|
|
156
162
|
const sessionsProps = {
|
|
157
163
|
styles: styles.wrapStyles,
|
|
@@ -148,7 +148,12 @@ export default auto(function Index (props) {
|
|
|
148
148
|
'is-mac': isMac,
|
|
149
149
|
'not-mac': !isMac,
|
|
150
150
|
'is-win': isWin,
|
|
151
|
-
|
|
151
|
+
// The left sidebar dock is desktop-only (see sidebar/index.jsx). This
|
|
152
|
+
// class drives the docked-layout CSS — the footer's left offset and the
|
|
153
|
+
// sidebar panel's top padding — so it has to follow the same gate as the
|
|
154
|
+
// layout maths in layout.jsx, or a pin persisted from a desktop session
|
|
155
|
+
// would restyle the mobile layout.
|
|
156
|
+
pinned: pinned && !store.isMobile,
|
|
152
157
|
'not-win': !isWin,
|
|
153
158
|
'qm-pinned': pinnedQuickCommandBar,
|
|
154
159
|
fullscreen,
|
|
@@ -226,6 +231,7 @@ export default auto(function Index (props) {
|
|
|
226
231
|
transferHistory: copiedHistory,
|
|
227
232
|
upgradeInfo,
|
|
228
233
|
pinned,
|
|
234
|
+
isMobile: store.isMobile,
|
|
229
235
|
leftSideBarIcons: config.leftSideBarIcons,
|
|
230
236
|
widgetInstancesLength: widgetInstances.length
|
|
231
237
|
}
|
|
@@ -253,7 +259,8 @@ export default auto(function Index (props) {
|
|
|
253
259
|
rightPanelPinned: store.rightPanelPinned,
|
|
254
260
|
rightPanelWidth: store.rightPanelWidth,
|
|
255
261
|
title: rightPanelTitle,
|
|
256
|
-
rightPanelTab
|
|
262
|
+
rightPanelTab,
|
|
263
|
+
isMobile: store.isMobile
|
|
257
264
|
}
|
|
258
265
|
const sshConfigProps = {
|
|
259
266
|
...pick(store, [
|
|
@@ -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
|
|
@@ -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
|
|
@@ -163,7 +163,11 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
163
163
|
inActiveTerminal,
|
|
164
164
|
leftSidePanelWidth,
|
|
165
165
|
leftSideBarWidth,
|
|
166
|
-
openedSideBar
|
|
166
|
+
openedSideBar,
|
|
167
|
+
rightPanelVisible,
|
|
168
|
+
rightPanelPinned,
|
|
169
|
+
rightPanelWidth,
|
|
170
|
+
isMobile
|
|
167
171
|
} = props
|
|
168
172
|
if ((!openQuickCommandBar && !pinnedQuickCommandBar) || !inActiveTerminal) {
|
|
169
173
|
return null
|
|
@@ -189,12 +193,26 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
189
193
|
: 'text'
|
|
190
194
|
const cls = classNames('qm-list-wrap')
|
|
191
195
|
const type = qmSortByFrequency ? 'primary' : 'default'
|
|
192
|
-
|
|
196
|
+
// Mirrors the footer's left offset. Mobile reserves nothing for the side
|
|
197
|
+
// panel — it is a full-screen drawer there (store.leftSidePanelWidth returns
|
|
198
|
+
// the viewport width), so adding it would push the popup off-screen.
|
|
199
|
+
const w = openedSideBar && !isMobile
|
|
200
|
+
? leftSideBarWidth + leftSidePanelWidth
|
|
201
|
+
: leftSideBarWidth
|
|
202
|
+
// Keep the popup's horizontal extent in step with the footer (same `w`
|
|
203
|
+
// expression above). A pinned right panel is a dock that reserves its width
|
|
204
|
+
// in layout.jsx, so give that width up here too — otherwise the popup's right
|
|
205
|
+
// end slides under the dock. Unpinned is an overlay and reserves nothing, and
|
|
206
|
+
// mobile is excluded to match layout.jsx.
|
|
207
|
+
const qmStyle = {
|
|
208
|
+
left: w
|
|
209
|
+
}
|
|
210
|
+
if (rightPanelVisible && rightPanelPinned && !isMobile) {
|
|
211
|
+
qmStyle.right = rightPanelWidth
|
|
212
|
+
}
|
|
193
213
|
const qmProps = {
|
|
194
214
|
className: 'qm-wrap-tooltip',
|
|
195
|
-
style:
|
|
196
|
-
left: w
|
|
197
|
-
}
|
|
215
|
+
style: qmStyle
|
|
198
216
|
}
|
|
199
217
|
return (
|
|
200
218
|
<div
|