@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
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Detail view for a widget instance: what it is, and what it has been doing.
3
+ *
4
+ * Shown in the right column of the widgets panel (Settings -> Widgets) when an
5
+ * instance is picked from the "Running instances" list, in place of the run
6
+ * form. The instance keeps this panel after it stops — being able to read the
7
+ * log of an instance that died is the whole point of it.
8
+ *
9
+ * Both panes come out of the instance's log file (see
10
+ * app/widgets/instance-log.js): on open, and again whenever Reload is pressed.
11
+ * Deliberately not on a timer — a log that scrolls under you is harder to read
12
+ * than one you refresh when you are ready.
13
+ *
14
+ * The strings here are English: the keys this panel needs do not exist yet, and
15
+ * a new key only reaches the app through an electerm-locales release plus a dep
16
+ * bump (see the locales notes). `e()` is used only for keys that do exist.
17
+ */
18
+ import { useCallback, useEffect, useRef, useState } from 'react'
19
+ import {
20
+ Alert,
21
+ Button,
22
+ Descriptions,
23
+ Popconfirm,
24
+ Segmented,
25
+ Space,
26
+ Tag,
27
+ Tooltip,
28
+ Typography
29
+ } from 'antd'
30
+ import {
31
+ CopyOutlined,
32
+ DeleteOutlined,
33
+ ReloadOutlined,
34
+ StopOutlined
35
+ } from '@ant-design/icons'
36
+ import { auto } from 'manate/react'
37
+ import { copy } from '../../common/clipboard'
38
+ import parseWidgetLog from '../../common/parse-widget-log'
39
+ import './widgets.styl'
40
+
41
+ const e = window.translate
42
+
43
+ const SECRET_KEY = /password|passwd|secret|token|apikey|api_key/i
44
+ const CONN_MARK = { ok: '✓', fail: '✗', info: '•' }
45
+
46
+ function pad (n, len = 2) {
47
+ return String(n).padStart(len, '0')
48
+ }
49
+
50
+ function formatTime (ts) {
51
+ const d = new Date(ts)
52
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`
53
+ }
54
+
55
+ // the URL of a widget that needs credentials carries them; the panel gets
56
+ // screenshotted, so hide the password here (copy still copies the real one)
57
+ function maskUrl (url) {
58
+ return String(url || '').replace(/\/\/([^:/@]+):([^@/]+)@/, '//$1:••••••@')
59
+ }
60
+
61
+ function displayValue (key, value) {
62
+ if (SECRET_KEY.test(key)) {
63
+ return value ? '••••••' : ''
64
+ }
65
+ if (typeof value === 'boolean') {
66
+ return value ? 'true' : 'false'
67
+ }
68
+ return value === undefined || value === null ? '' : String(value)
69
+ }
70
+
71
+ export default auto(function WidgetInstanceDetail ({ instance, store }) {
72
+ const [pane, setPane] = useState('log')
73
+ const [data, setData] = useState({ logs: [], events: [] })
74
+ // which confirm popup is open ('' | 'clear' | 'stop'), so its tooltip can
75
+ // step aside — both are anchored to the same button, and the tooltip would
76
+ // otherwise sit on top of the confirm button and swallow the click
77
+ const [confirming, setConfirming] = useState('')
78
+ const [status, setStatus] = useState({
79
+ loaded: false,
80
+ loading: false,
81
+ exists: true,
82
+ error: ''
83
+ })
84
+ const paneRef = useRef(null)
85
+
86
+ const running = store.widgetInstances.some(w => w.id === instance.id)
87
+ const logPath = store.widgetLogPath && window.reqs?.path?.join
88
+ ? window.reqs.path.join(store.widgetLogPath, `${instance.id}.log`)
89
+ : ''
90
+
91
+ const readLog = useCallback(async () => {
92
+ if (!logPath) {
93
+ setStatus({ loaded: true, loading: false, exists: false, error: '' })
94
+ return
95
+ }
96
+ setStatus(s => ({ ...s, loading: true }))
97
+ try {
98
+ // read straight through: fs/promises has no `exists`, and the bridge
99
+ // rejects unknown helpers — so a missing file is detected from ENOENT
100
+ // rather than probed for, which also saves a round trip
101
+ const text = await window.fs.readFile(logPath)
102
+ setData(parseWidgetLog(text))
103
+ setStatus({ loaded: true, loading: false, exists: true, error: '' })
104
+ } catch (err) {
105
+ const msg = (err && err.message) || String(err)
106
+ if (/ENOENT|no such file/i.test(msg)) {
107
+ // the normal first state of a fresh instance, not a failure
108
+ setData({ logs: [], events: [] })
109
+ setStatus({ loaded: true, loading: false, exists: false, error: '' })
110
+ return
111
+ }
112
+ setStatus({ loaded: true, loading: false, exists: true, error: msg })
113
+ }
114
+ }, [logPath])
115
+
116
+ useEffect(() => {
117
+ readLog()
118
+ }, [readLog])
119
+
120
+ // newest entry is at the bottom, so land there after every load
121
+ useEffect(() => {
122
+ const el = paneRef.current
123
+ if (el) {
124
+ el.scrollTop = el.scrollHeight
125
+ }
126
+ }, [data, pane])
127
+
128
+ const clearLog = async () => {
129
+ if (!logPath) {
130
+ return
131
+ }
132
+ try {
133
+ // the bridge resolves false instead of rejecting when the write fails
134
+ const ok = await window.fs.writeFile(logPath, '')
135
+ if (ok === false) {
136
+ throw new Error('could not write ' + logPath)
137
+ }
138
+ setData({ logs: [], events: [] })
139
+ setStatus(s => ({ ...s, exists: true, error: '' }))
140
+ } catch (err) {
141
+ setStatus(s => ({ ...s, error: err.message || String(err) }))
142
+ }
143
+ }
144
+
145
+ // Stopping is the one log change the user causes directly, so refresh after
146
+ // it — that is not the polling the panel is avoiding, it is the panel
147
+ // agreeing with the button that was just pressed.
148
+ const stopInstance = async () => {
149
+ await store.stopWidget(instance.id)
150
+ readLog()
151
+ }
152
+
153
+ const infoItems = [
154
+ {
155
+ key: 'widget',
156
+ label: 'Widget',
157
+ children: instance.widgetId
158
+ },
159
+ {
160
+ key: 'instance',
161
+ label: 'Instance',
162
+ children: <span className='wil-mono'>{instance.id}</span>
163
+ },
164
+ {
165
+ key: 'status',
166
+ label: 'Status',
167
+ children: running ? <Tag color='green'>running</Tag> : <Tag>stopped</Tag>
168
+ }
169
+ ]
170
+ if (instance.serverInfo && instance.serverInfo.url) {
171
+ infoItems.push({
172
+ key: 'url',
173
+ label: 'URL',
174
+ children: (
175
+ <span className='wil-mono' title={maskUrl(instance.serverInfo.url)}>
176
+ {maskUrl(instance.serverInfo.url)}
177
+ </span>
178
+ )
179
+ })
180
+ }
181
+ if (instance.serverInfo && instance.serverInfo.path) {
182
+ infoItems.push({
183
+ key: 'path',
184
+ label: 'Path',
185
+ children: <span className='wil-mono'>{instance.serverInfo.path}</span>
186
+ })
187
+ }
188
+ if (instance.startedAt) {
189
+ infoItems.push({
190
+ key: 'startedAt',
191
+ label: 'Started',
192
+ children: new Date(instance.startedAt).toLocaleString()
193
+ })
194
+ }
195
+ for (const key of Object.keys(instance.config || {})) {
196
+ if (key === 'autoRun') {
197
+ continue
198
+ }
199
+ infoItems.push({
200
+ key: 'config-' + key,
201
+ label: key,
202
+ children: <span className='wil-mono'>{displayValue(key, instance.config[key])}</span>
203
+ })
204
+ }
205
+
206
+ let emptyText = 'Nothing logged yet. Press reload after the widget has been used.'
207
+ if (!logPath) {
208
+ emptyText = 'The widget log directory is not available in this environment.'
209
+ } else if (!status.exists) {
210
+ emptyText = 'No log file yet — this instance has not written anything.'
211
+ }
212
+
213
+ const renderPane = () => {
214
+ const isLog = pane === 'log'
215
+ const entries = isLog ? data.logs : data.events
216
+ return (
217
+ <>
218
+ {
219
+ status.error
220
+ ? (
221
+ <Alert
222
+ className='mg1y'
223
+ type='error'
224
+ showIcon
225
+ message={status.error}
226
+ />
227
+ )
228
+ : null
229
+ }
230
+ <div className='wil-pane' ref={paneRef}>
231
+ {entries.map((entry, i) => {
232
+ const state = isLog
233
+ ? entry.level
234
+ : (entry.ok === null ? 'info' : (entry.ok ? 'ok' : 'fail'))
235
+ return (
236
+ <div
237
+ key={i}
238
+ className={'wil-line wil-' + state}
239
+ >
240
+ <span
241
+ className='wil-time'
242
+ title={new Date(entry.ts).toISOString()}
243
+ >
244
+ {formatTime(entry.ts)}
245
+ </span>
246
+ {isLog
247
+ ? null
248
+ : <span className='wil-mark'>{CONN_MARK[state]}</span>}
249
+ {isLog ? null : <span className='wil-type'>{entry.type}</span>}
250
+ {isLog || !entry.from
251
+ ? null
252
+ : <span className='wil-from'>{entry.from}</span>}
253
+ <span className='wil-msg'>{entry.msg}</span>
254
+ </div>
255
+ )
256
+ })}
257
+ {entries.length ? null : <div className='wil-empty'>{emptyText}</div>}
258
+ </div>
259
+ {logPath
260
+ ? (
261
+ <div className='wil-path elli' title={logPath}>
262
+ Log file: {logPath}
263
+ </div>
264
+ )
265
+ : null}
266
+ </>
267
+ )
268
+ }
269
+
270
+ return (
271
+ <div className='widget-instance-detail'>
272
+ <div className='wil-head'>
273
+ <Typography.Text
274
+ strong
275
+ ellipsis
276
+ className='wil-head-title'
277
+ title={instance.title}
278
+ >
279
+ {instance.title}
280
+ </Typography.Text>
281
+ <Space size={4}>
282
+ <Tooltip title='Reload log'>
283
+ <Button
284
+ size='small'
285
+ icon={<ReloadOutlined />}
286
+ loading={status.loading}
287
+ onClick={readLog}
288
+ />
289
+ </Tooltip>
290
+ {instance.serverInfo && instance.serverInfo.url
291
+ ? (
292
+ <Tooltip title='Copy URL'>
293
+ <Button
294
+ size='small'
295
+ icon={<CopyOutlined />}
296
+ onClick={() => copy(instance.serverInfo.url)}
297
+ />
298
+ </Tooltip>
299
+ )
300
+ : null}
301
+ {logPath
302
+ ? (
303
+ <Popconfirm
304
+ title='Clear this instance log?'
305
+ okText='Clear'
306
+ cancelText={e('cancel')}
307
+ onOpenChange={open => setConfirming(open ? 'clear' : '')}
308
+ onConfirm={clearLog}
309
+ >
310
+ <Tooltip title='Clear log' open={confirming === 'clear' ? false : undefined}>
311
+ <Button size='small' icon={<DeleteOutlined />} />
312
+ </Tooltip>
313
+ </Popconfirm>
314
+ )
315
+ : null}
316
+ {running
317
+ ? (
318
+ <Popconfirm
319
+ title='Stop this widget?'
320
+ okText='Stop'
321
+ cancelText={e('cancel')}
322
+ onOpenChange={open => setConfirming(open ? 'stop' : '')}
323
+ onConfirm={stopInstance}
324
+ >
325
+ <Tooltip title='Stop' open={confirming === 'stop' ? false : undefined}>
326
+ <Button
327
+ size='small'
328
+ danger
329
+ icon={<StopOutlined />}
330
+ />
331
+ </Tooltip>
332
+ </Popconfirm>
333
+ )
334
+ : (
335
+ <Button
336
+ size='small'
337
+ onClick={() => store.closeWidgetInstance()}
338
+ >
339
+ Back
340
+ </Button>
341
+ )}
342
+ </Space>
343
+ </div>
344
+ <Descriptions
345
+ className='wil-descriptions'
346
+ size='small'
347
+ bordered
348
+ column={1}
349
+ items={infoItems}
350
+ />
351
+ <Segmented
352
+ className='wil-switch'
353
+ size='small'
354
+ value={pane}
355
+ onChange={setPane}
356
+ options={[
357
+ {
358
+ label: `Running log (${data.logs.length})`,
359
+ value: 'log'
360
+ },
361
+ {
362
+ label: `Connections (${data.events.length})`,
363
+ value: 'conn'
364
+ }
365
+ ]}
366
+ />
367
+ {renderPane()}
368
+ </div>
369
+ )
370
+ })
@@ -11,10 +11,11 @@ import { auto } from 'manate/react'
11
11
 
12
12
  const e = window.translate
13
13
 
14
- export default auto(function WidgetInstance ({ item }) {
14
+ export default auto(function WidgetInstance ({ item, active, onClick }) {
15
15
  const { id, title, serverInfo, autoRun } = item
16
16
  const cls = classnames('item-list-unit', {
17
- 'autorun-active': autoRun
17
+ 'autorun-active': autoRun,
18
+ active
18
19
  })
19
20
  const delProps = {
20
21
  title: e('del'),
@@ -28,6 +29,16 @@ export default auto(function WidgetInstance ({ item }) {
28
29
  function onConfirm () {
29
30
  window.store.stopWidget(id)
30
31
  }
32
+ // the row itself opens the instance detail, so the action icons must not
33
+ // also trigger it
34
+ function stopPropagation (evt) {
35
+ evt.stopPropagation()
36
+ }
37
+ function handleClick () {
38
+ if (onClick) {
39
+ onClick(item)
40
+ }
41
+ }
31
42
  const popProps = {
32
43
  title: e('del') + '?',
33
44
  onConfirm,
@@ -45,7 +56,7 @@ export default auto(function WidgetInstance ({ item }) {
45
56
  }
46
57
  const popoverContent = serverInfo
47
58
  ? (
48
- <div>
59
+ <div className='wil-pop-info'>
49
60
  <div style={{ display: 'flex', alignItems: 'center' }}>
50
61
  <span>URL: {serverInfo.url}</span>
51
62
  <CopyOutlined
@@ -70,6 +81,7 @@ export default auto(function WidgetInstance ({ item }) {
70
81
  <div
71
82
  key={id}
72
83
  className={cls}
84
+ onClick={handleClick}
73
85
  >
74
86
  {
75
87
  serverInfo
@@ -84,17 +96,22 @@ export default auto(function WidgetInstance ({ item }) {
84
96
  )
85
97
  : titleDiv
86
98
  }
87
- <Tooltip title='Toggle auto-run'>
88
- <ThunderboltOutlined
89
- className='pointer list-item-autorun'
90
- onClick={handleToggleAutoRun}
91
- />
92
- </Tooltip>
93
- <Popconfirm
94
- {...popProps}
99
+ <div
100
+ className='wil-item-actions'
101
+ onClick={stopPropagation}
95
102
  >
96
- {icon}
97
- </Popconfirm>
103
+ <Tooltip title='Toggle auto-run'>
104
+ <ThunderboltOutlined
105
+ className='pointer list-item-autorun'
106
+ onClick={handleToggleAutoRun}
107
+ />
108
+ </Tooltip>
109
+ <Popconfirm
110
+ {...popProps}
111
+ >
112
+ {icon}
113
+ </Popconfirm>
114
+ </div>
98
115
  </div>
99
116
  )
100
117
  })
@@ -1,10 +1,12 @@
1
1
  import WidgetInstance from './widget-instance'
2
2
 
3
- export default function WidgetInstances ({ widgetInstances }) {
3
+ export default function WidgetInstances ({ widgetInstances, activeItemId, onClickItem }) {
4
4
  return widgetInstances.map(item => (
5
5
  <WidgetInstance
6
6
  key={item.id}
7
7
  item={item}
8
+ active={activeItemId === item.id}
9
+ onClick={onClickItem}
8
10
  />
9
11
  ))
10
12
  }
@@ -138,10 +138,18 @@ export default auto(function WidgetsList ({ activeItemId, store, onClickItem })
138
138
  )
139
139
  }
140
140
 
141
+ // clicking a running instance shows its detail + log in the right column
142
+ // (the widgets list above drills into the run form instead)
143
+ const onClickWidgetInstance = (instance) => {
144
+ window.store.selectWidgetInstance(instance)
145
+ }
146
+
141
147
  const renderInstancesSection = () => {
142
148
  return (
143
149
  <WidgetInstances
144
150
  widgetInstances={widgetInstances}
151
+ activeItemId={activeItemId}
152
+ onClickItem={onClickWidgetInstance}
145
153
  />
146
154
  )
147
155
  }
@@ -0,0 +1,100 @@
1
+
2
+ // hover popover on a running-instance row. A long path makes it wider than the
3
+ // list column, and it then spills over the detail panel's heading, so it is
4
+ // held to the column and wrapped instead.
5
+ .wil-pop-info
6
+ max-width 220px
7
+ word-break break-all
8
+
9
+ // widget manager: instance detail view (widget-instance-detail.jsx)
10
+ .widget-instance-detail
11
+ display flex
12
+ flex-direction column
13
+ min-height 0
14
+ height 100%
15
+ .wil-head
16
+ display flex
17
+ align-items center
18
+ justify-content space-between
19
+ gap 8px
20
+ margin-bottom 10px
21
+ flex-shrink 0
22
+ .wil-head-title
23
+ min-width 0
24
+ font-size 14px
25
+ .wil-descriptions
26
+ // the log is the point of this panel, so a long config list gives up room
27
+ // (and scrolls) instead of squeezing it: shrinkable, and allowed to go below
28
+ // its content height, with the pane holding a floor of its own
29
+ flex 0 1 auto
30
+ min-height 0
31
+ overflow auto
32
+ // long values (paths, URLs, keys) must wrap instead of widening the panel
33
+ .ant-descriptions-item-label
34
+ width 90px
35
+ white-space nowrap
36
+ .ant-descriptions-item-content
37
+ word-break break-all
38
+ .wil-mono
39
+ font-family monospace
40
+ font-size 12px
41
+ .wil-switch
42
+ margin 10px 0 6px
43
+ flex-shrink 0
44
+ .wil-pane
45
+ flex 1
46
+ // a floor, so the log stays readable no matter how long the config list is
47
+ min-height 200px
48
+ overflow auto
49
+ padding 6px 8px
50
+ background var(--main-dark)
51
+ border 1px solid var(--main-light)
52
+ border-radius 4px
53
+ font-family monospace
54
+ font-size 12px
55
+ line-height 1.6
56
+ .wil-line
57
+ display flex
58
+ gap 6px
59
+ white-space pre-wrap
60
+ word-break break-word
61
+ border-bottom 1px solid var(--main-light)
62
+ &:last-child
63
+ border-bottom 0
64
+ .wil-time
65
+ flex-shrink 0
66
+ color var(--text-disabled)
67
+ .wil-mark
68
+ .wil-type
69
+ flex-shrink 0
70
+ .wil-from
71
+ flex-shrink 0
72
+ color var(--text-dark)
73
+ .wil-msg
74
+ min-width 0
75
+ .wil-debug
76
+ color var(--text-dark)
77
+ .wil-log
78
+ color var(--text)
79
+ .wil-warn
80
+ color var(--warn)
81
+ .wil-error
82
+ color var(--error)
83
+ .wil-ok
84
+ color var(--success)
85
+ .wil-mark
86
+ color var(--success)
87
+ .wil-fail
88
+ color var(--error)
89
+ .wil-mark
90
+ color var(--error)
91
+ .wil-info
92
+ color var(--text-dark)
93
+ .wil-empty
94
+ color var(--text-dark)
95
+ padding 6px 0
96
+ .wil-path
97
+ flex-shrink 0
98
+ margin-top 6px
99
+ color var(--text-disabled)
100
+ font-size 11px