@electerm/electerm-react 5.3.26 → 5.5.6

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 (134) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +20 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +14 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/common/item-filter.jsx +76 -0
  29. package/client/components/common/item-filter.styl +54 -0
  30. package/client/components/common/lazy-boundary.jsx +34 -0
  31. package/client/components/common/opacity.jsx +3 -1
  32. package/client/components/footer/footer-entry.jsx +35 -5
  33. package/client/components/footer/footer.styl +8 -1
  34. package/client/components/layout/layout.jsx +23 -31
  35. package/client/components/layout/pixed.js +3 -0
  36. package/client/components/main/main.jsx +55 -28
  37. package/client/components/main/term-fullscreen.styl +9 -3
  38. package/client/components/quick-commands/qm.styl +3 -2
  39. package/client/components/rdp/file-transfer.js +4 -4
  40. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  41. package/client/components/remote-monitor/monitor-details.styl +87 -0
  42. package/client/components/remote-monitor/monitor-model.js +764 -0
  43. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  44. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  45. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  46. package/client/components/remote-monitor/session-monitor.js +361 -0
  47. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  48. package/client/components/remote-monitor/visibility.js +18 -0
  49. package/client/components/session/session-control.jsx +29 -2
  50. package/client/components/session/session.jsx +40 -18
  51. package/client/components/session/sessions.jsx +5 -2
  52. package/client/components/setting-panel/setting-common.jsx +25 -56
  53. package/client/components/setting-panel/setting-modal.jsx +57 -42
  54. package/client/components/setting-panel/setting-terminal.jsx +7 -2
  55. package/client/components/setting-panel/setting.styl +1 -1
  56. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  57. package/client/components/sftp/file-compare-modal.jsx +6 -3
  58. package/client/components/sftp/file-item.jsx +5 -3
  59. package/client/components/sftp/owner-list.js +11 -2
  60. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  61. package/client/components/sys-menu/menu-btn.jsx +30 -1
  62. package/client/components/tabs/index.jsx +2 -2
  63. package/client/components/tabs/tab-title.jsx +3 -2
  64. package/client/components/tabs/tab.jsx +5 -1
  65. package/client/components/terminal/attach-addon-custom.js +295 -49
  66. package/client/components/terminal/automation/index.js +64 -0
  67. package/client/components/terminal/automation/keys.js +49 -0
  68. package/client/components/terminal/automation/stream-matcher.js +82 -0
  69. package/client/components/terminal/automation/strip-ansi.js +19 -0
  70. package/client/components/terminal/automation/trigger-engine.js +233 -0
  71. package/client/components/terminal/automation/trigger-presets.js +110 -0
  72. package/client/components/terminal/command-tracker-addon.js +31 -1
  73. package/client/components/terminal/mixins/index.js +27 -0
  74. package/client/components/terminal/mixins/term-attach.js +123 -0
  75. package/client/components/terminal/mixins/term-config.jsx +84 -0
  76. package/client/components/terminal/mixins/term-content.js +88 -0
  77. package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
  78. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  79. package/client/components/terminal/mixins/term-init.js +122 -0
  80. package/client/components/terminal/mixins/term-log.jsx +96 -0
  81. package/client/components/terminal/mixins/term-resize.js +87 -0
  82. package/client/components/terminal/mixins/term-socket.js +433 -0
  83. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  84. package/client/components/terminal/mixins/term-theme.js +190 -0
  85. package/client/components/terminal/mixins/term-touch.js +136 -0
  86. package/client/components/terminal/shell-detect.js +37 -0
  87. package/client/components/terminal/shell.js +0 -28
  88. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  89. package/client/components/terminal/shortcut-bar.jsx +4 -6
  90. package/client/components/terminal/shortcut-bar.styl +4 -0
  91. package/client/components/terminal/ssh-reload-state.js +56 -0
  92. package/client/components/terminal/startup-queue.js +379 -0
  93. package/client/components/terminal/terminal-interactive.jsx +32 -0
  94. package/client/components/terminal/terminal.jsx +102 -2007
  95. package/client/components/terminal/transfer-client-base.js +5 -29
  96. package/client/components/terminal/trzsz-client.js +1 -3
  97. package/client/components/terminal/xmodem-client.js +1 -3
  98. package/client/components/terminal/xterm-loader.js +7 -0
  99. package/client/components/terminal/zmodem-client.js +1 -3
  100. package/client/components/terminal-info/base.jsx +18 -70
  101. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  102. package/client/components/terminal-info/terminal-info.jsx +50 -59
  103. package/client/components/terminal-info/terminal-info.styl +6 -49
  104. package/client/components/text-editor/open-text-editor.js +30 -0
  105. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  106. package/client/components/text-editor/text-editor.jsx +14 -0
  107. package/client/components/triggers/trigger-editor.jsx +246 -0
  108. package/client/components/triggers/trigger-form.jsx +200 -0
  109. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  110. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  111. package/client/components/triggers/trigger-transport.jsx +60 -0
  112. package/client/components/triggers/triggers-field.jsx +19 -0
  113. package/client/components/widgets/widgets-list.jsx +11 -1
  114. package/client/css/mobile.styl +9 -1
  115. package/client/store/common.js +12 -1
  116. package/client/store/init-state.js +7 -0
  117. package/client/store/mcp-handler.js +158 -30
  118. package/client/store/setting.js +9 -0
  119. package/client/store/store.js +2 -0
  120. package/client/store/sync.js +2 -0
  121. package/client/store/tab.js +41 -3
  122. package/client/store/trigger.js +118 -0
  123. package/client/store/watch.js +13 -5
  124. package/client/views/index.pug +3 -0
  125. package/package.json +1 -1
  126. package/client/components/terminal-info/activity.jsx +0 -57
  127. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  128. package/client/components/terminal-info/disk.jsx +0 -45
  129. package/client/components/terminal-info/format-disks.js +0 -45
  130. package/client/components/terminal-info/network.jsx +0 -121
  131. package/client/components/terminal-info/resource.jsx +0 -99
  132. package/client/components/terminal-info/run-cmd.jsx +0 -263
  133. package/client/components/terminal-info/sys-info.jsx +0 -56
  134. package/client/components/terminal-info/up.jsx +0 -17
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Reusable declarative-trigger editor.
3
+ * Controlled component: props { value, onChange }.
4
+ * Used by bookmark form tab, footer session modal and anywhere else.
5
+ * Supports UI editing, JSON editing and built-in presets.
6
+ */
7
+ import { useState } from 'react'
8
+ import {
9
+ Button,
10
+ Switch,
11
+ Modal,
12
+ Form,
13
+ Input,
14
+ Dropdown,
15
+ Popconfirm,
16
+ Empty,
17
+ Tag,
18
+ Tooltip,
19
+ message
20
+ } from 'antd'
21
+ import {
22
+ PlusOutlined,
23
+ CopyOutlined,
24
+ DeleteOutlined,
25
+ EditOutlined,
26
+ CodeOutlined,
27
+ ThunderboltOutlined
28
+ } from '@ant-design/icons'
29
+ import TriggerForm, { buildTriggerFromFormValues } from './trigger-form.jsx'
30
+ import {
31
+ triggerPresets,
32
+ buildTriggerFromPreset,
33
+ buildEmptyTrigger
34
+ } from '../terminal/automation/trigger-presets.js'
35
+ import { validateTriggers } from '../terminal/automation/trigger-engine.js'
36
+ import uid from '../../common/uid'
37
+
38
+ const e = window.translate
39
+
40
+ export function matchSummary (t) {
41
+ const type = t.match?.type === 'regex' ? 're' : 'text'
42
+ const v = t.match?.value || ''
43
+ const short = v.length > 42 ? v.slice(0, 42) + '…' : v
44
+ return `[${type}] ${short}`
45
+ }
46
+
47
+ export function actionSummary (t) {
48
+ if (t.action?.type === 'notify') {
49
+ return 'notify'
50
+ }
51
+ const v = t.action?.value || ''
52
+ const short = v.length > 24 ? v.slice(0, 24) + '…' : JSON.stringify(v)
53
+ return `send ${short}${t.sendEnter === false ? '' : ' +⏎'}`
54
+ }
55
+
56
+ export default function TriggerEditor ({ value, onChange }) {
57
+ const list = Array.isArray(value) ? value : []
58
+ const [editing, setEditing] = useState(null)
59
+ const [jsonOpen, setJsonOpen] = useState(false)
60
+ const [jsonText, setJsonText] = useState('')
61
+ const [form] = Form.useForm()
62
+
63
+ const emit = (next) => {
64
+ onChange?.(next)
65
+ }
66
+
67
+ const handleToggle = (id, enabled) => {
68
+ emit(list.map(t => t.id === id ? { ...t, enabled } : t))
69
+ }
70
+
71
+ const handleDel = (id) => {
72
+ emit(list.filter(t => t.id !== id))
73
+ }
74
+
75
+ const handleDuplicate = (item) => {
76
+ const copy = { ...JSON.parse(JSON.stringify(item)), id: uid(), name: (item.name || '') + ' (copy)' }
77
+ emit([...list, copy])
78
+ }
79
+
80
+ const openAdd = () => {
81
+ setEditing(buildEmptyTrigger(uid))
82
+ }
83
+
84
+ const openEdit = (item) => {
85
+ setEditing(JSON.parse(JSON.stringify(item)))
86
+ }
87
+
88
+ const handlePreset = ({ key }) => {
89
+ const preset = triggerPresets[Number(key)]
90
+ if (!preset) {
91
+ return
92
+ }
93
+ emit([...list, buildTriggerFromPreset(preset, uid)])
94
+ message.success('OK')
95
+ }
96
+
97
+ const handleSaveEdit = async () => {
98
+ try {
99
+ const v = await form.validateFields()
100
+ const next = buildTriggerFromFormValues(editing, v)
101
+ const errors = validateTriggers([next])
102
+ if (errors.length) {
103
+ message.error(errors[0])
104
+ return
105
+ }
106
+ const exists = list.some(t => t.id === next.id)
107
+ emit(exists ? list.map(t => t.id === next.id ? next : t) : [...list, next])
108
+ setEditing(null)
109
+ } catch (err) {
110
+ // validation failed, keep modal open
111
+ }
112
+ }
113
+
114
+ const openJson = () => {
115
+ setJsonText(JSON.stringify(list, null, 2))
116
+ setJsonOpen(true)
117
+ }
118
+
119
+ const applyJson = () => {
120
+ let parsed
121
+ try {
122
+ parsed = JSON.parse(jsonText)
123
+ } catch (err) {
124
+ message.error('Invalid JSON: ' + err.message)
125
+ return
126
+ }
127
+ if (!Array.isArray(parsed)) {
128
+ message.error('JSON must be an array')
129
+ return
130
+ }
131
+ const normalized = parsed.map(t => ({
132
+ ...t,
133
+ id: t.id || uid()
134
+ }))
135
+ const errors = validateTriggers(normalized)
136
+ if (errors.length) {
137
+ message.error(errors[0])
138
+ return
139
+ }
140
+ emit(normalized)
141
+ setJsonOpen(false)
142
+ message.success('OK')
143
+ }
144
+
145
+ const presetMenu = {
146
+ items: triggerPresets.map((p, i) => ({
147
+ key: String(i),
148
+ label: p.name
149
+ })),
150
+ onClick: handlePreset
151
+ }
152
+
153
+ return (
154
+ <div className='trigger-editor'>
155
+ <div className='pd1b' style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
156
+ <Button
157
+ size='small'
158
+ type='primary'
159
+ icon={<PlusOutlined />}
160
+ onClick={openAdd}
161
+ >
162
+ {e('new')}
163
+ </Button>
164
+ <Dropdown menu={presetMenu} trigger={['click']}>
165
+ <Button size='small' icon={<ThunderboltOutlined />}>
166
+ {e('presets')}
167
+ </Button>
168
+ </Dropdown>
169
+ <Button
170
+ size='small'
171
+ icon={<CodeOutlined />}
172
+ onClick={openJson}
173
+ >
174
+ JSON
175
+ </Button>
176
+ </div>
177
+ {
178
+ !list.length
179
+ ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description='No triggers yet — add one or pick a preset' />
180
+ : list.map(t => (
181
+ <div
182
+ key={t.id}
183
+ className='trigger-item pd1x pd1y mg1b'
184
+ style={{ border: '1px solid var(--border, #333)', borderRadius: 4 }}
185
+ >
186
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
187
+ <Switch
188
+ size='small'
189
+ checked={t.enabled !== false}
190
+ onChange={(v) => handleToggle(t.id, v)}
191
+ />
192
+ <b className='elli' style={{ flex: 1 }} title={t.name}>
193
+ {t.name || 'Unnamed'}
194
+ </b>
195
+ <Tag>{t.mode || 'cooldown'}</Tag>
196
+ <Tooltip title={e('edit')}>
197
+ <EditOutlined className='pointer' onClick={() => openEdit(t)} />
198
+ </Tooltip>
199
+ <Tooltip title={e('duplicate')}>
200
+ <CopyOutlined className='pointer' onClick={() => handleDuplicate(t)} />
201
+ </Tooltip>
202
+ <Popconfirm
203
+ title={e('del')}
204
+ onConfirm={() => handleDel(t.id)}
205
+ >
206
+ <DeleteOutlined className='pointer' />
207
+ </Popconfirm>
208
+ </div>
209
+ <div className='mg1t small muted elli' title={matchSummary(t) + ' → ' + actionSummary(t)}>
210
+ {matchSummary(t)} → {actionSummary(t)}
211
+ </div>
212
+ </div>
213
+ ))
214
+ }
215
+ <Modal
216
+ open={!!editing}
217
+ title={(editing?.name ? e('edit') : e('new')) + ' ' + e('triggers')}
218
+ onCancel={() => setEditing(null)}
219
+ onOk={handleSaveEdit}
220
+ destroyOnHidden
221
+ width={640}
222
+ >
223
+ {editing ? <TriggerForm form={form} initial={editing} /> : null}
224
+ </Modal>
225
+ <Modal
226
+ open={jsonOpen}
227
+ title='JSON'
228
+ onCancel={() => setJsonOpen(false)}
229
+ onOk={applyJson}
230
+ destroyOnHidden
231
+ width={640}
232
+ >
233
+ <Input.TextArea
234
+ rows={14}
235
+ value={jsonText}
236
+ onChange={(ev) => setJsonText(ev.target.value)}
237
+ spellCheck={false}
238
+ style={{ fontFamily: 'monospace' }}
239
+ />
240
+ <div className='mg1t small muted'>
241
+ JSON array of trigger rules. Invalid regex or missing match will be rejected.
242
+ </div>
243
+ </Modal>
244
+ </div>
245
+ )
246
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Single trigger rule form (used inside a Modal by TriggerEditor).
3
+ * Controlled via antd Form, initialValues = editing rule.
4
+ */
5
+ import { useEffect } from 'react'
6
+ import {
7
+ Button,
8
+ Dropdown,
9
+ Form,
10
+ Input,
11
+ Switch,
12
+ Select,
13
+ InputNumber,
14
+ Radio
15
+ } from 'antd'
16
+ import { CaretDownOutlined } from '@ant-design/icons'
17
+ import {
18
+ triggerActionTypes,
19
+ triggerMatchTypes,
20
+ triggerModes,
21
+ triggerPresets
22
+ } from '../terminal/automation/trigger-presets.js'
23
+ import uid from '../../common/uid'
24
+
25
+ const e = window.translate
26
+
27
+ // Build a full trigger rule from form values. `editing` is the rule being
28
+ // edited (or a {id:''} placeholder for a new one).
29
+ export function buildTriggerFromFormValues (editing = {}, v) {
30
+ return {
31
+ ...(editing.id ? editing : {}),
32
+ id: editing.id || uid(),
33
+ name: v.name,
34
+ enabled: v.enabled !== false,
35
+ match: {
36
+ type: v.matchType,
37
+ value: v.matchValue,
38
+ caseSensitive: !!v.caseSensitive
39
+ },
40
+ action: {
41
+ type: v.actionType,
42
+ value: v.actionType === 'notify' ? '' : (v.actionValue || '')
43
+ },
44
+ sendEnter: v.sendEnter !== false,
45
+ mode: v.mode,
46
+ // Keep the value while another mode is selected so switching back to
47
+ // cooldown does not silently replace the user's interval.
48
+ cooldownMs: v.cooldownMs == null ? 500 : v.cooldownMs
49
+ }
50
+ }
51
+
52
+ export default function TriggerForm ({ form, initial }) {
53
+ const actionType = Form.useWatch('actionType', form)
54
+ const mode = Form.useWatch('mode', form)
55
+
56
+ useEffect(() => {
57
+ form.resetFields()
58
+ form.setFieldsValue({
59
+ name: initial.name || '',
60
+ enabled: initial.enabled !== false,
61
+ matchType: initial.match?.type || 'text',
62
+ matchValue: initial.match?.value || '',
63
+ caseSensitive: !!initial.match?.caseSensitive,
64
+ actionType: initial.action?.type || 'send',
65
+ actionValue: initial.action?.value || '',
66
+ sendEnter: initial.sendEnter !== false,
67
+ mode: initial.mode || 'cooldown',
68
+ cooldownMs: initial.cooldownMs == null ? 500 : initial.cooldownMs
69
+ })
70
+ }, [form, initial])
71
+
72
+ const presetMenu = {
73
+ items: triggerPresets.map((p, i) => ({
74
+ key: String(i),
75
+ label: p.name
76
+ })),
77
+ onClick: ({ key }) => {
78
+ const p = triggerPresets[Number(key)]
79
+ if (!p) {
80
+ return
81
+ }
82
+ form.setFieldsValue({
83
+ name: p.name,
84
+ enabled: true,
85
+ matchType: p.match.type,
86
+ matchValue: p.match.value,
87
+ caseSensitive: !!p.match.caseSensitive,
88
+ actionType: p.action.type,
89
+ actionValue: p.action.value,
90
+ sendEnter: p.sendEnter !== false,
91
+ mode: p.mode || 'cooldown',
92
+ cooldownMs: p.cooldownMs == null ? 500 : p.cooldownMs
93
+ })
94
+ }
95
+ }
96
+
97
+ return (
98
+ <Form
99
+ form={form}
100
+ layout='vertical'
101
+ >
102
+ <div className='pd1b alignright'>
103
+ <Dropdown menu={presetMenu} trigger={['click']}>
104
+ <Button icon={<CaretDownOutlined />}>
105
+ {e('presets')}
106
+ </Button>
107
+ </Dropdown>
108
+ </div>
109
+ <Form.Item
110
+ name='name'
111
+ label={e('title')}
112
+ rules={[{ required: true, message: e('title') + ' required' }]}
113
+ >
114
+ <Input placeholder='Cisco --More-- auto pager' />
115
+ </Form.Item>
116
+ <Form.Item
117
+ name='enabled'
118
+ label={e('enabled')}
119
+ valuePropName='checked'
120
+ >
121
+ <Switch />
122
+ </Form.Item>
123
+ <Form.Item label='Match'>
124
+ <div style={{ display: 'flex', gap: 8 }}>
125
+ <Form.Item name='matchType' noStyle>
126
+ <Radio.Group
127
+ options={triggerMatchTypes.map(t => ({ label: t.label, value: t.value }))}
128
+ optionType='button'
129
+ buttonStyle='solid'
130
+ size='small'
131
+ />
132
+ </Form.Item>
133
+ <Form.Item
134
+ name='matchValue'
135
+ noStyle
136
+ rules={[{ required: true, message: 'match required' }]}
137
+ >
138
+ <Input
139
+ placeholder='--More-- or press any key.*'
140
+ style={{ flex: 1 }}
141
+ />
142
+ </Form.Item>
143
+ </div>
144
+ </Form.Item>
145
+ <Form.Item
146
+ name='caseSensitive'
147
+ label='Case sensitive'
148
+ valuePropName='checked'
149
+ >
150
+ <Switch size='small' />
151
+ </Form.Item>
152
+ <Form.Item label='Action'>
153
+ <div style={{ display: 'flex', gap: 8 }}>
154
+ <Form.Item name='actionType' noStyle>
155
+ <Select
156
+ style={{ width: 130 }}
157
+ options={triggerActionTypes.map(t => ({ label: t.label, value: t.value }))}
158
+ />
159
+ </Form.Item>
160
+ <Form.Item name='actionValue' noStyle>
161
+ <Input
162
+ placeholder={actionType === 'notify' ? 'Notify only (no send)' : 'space, y, \\r, \\x03, ^M'}
163
+ style={{ flex: 1 }}
164
+ disabled={actionType === 'notify'}
165
+ />
166
+ </Form.Item>
167
+ </div>
168
+ </Form.Item>
169
+ <div style={{ display: 'flex', gap: 24 }}>
170
+ <Form.Item
171
+ name='sendEnter'
172
+ label='Send Enter'
173
+ valuePropName='checked'
174
+ >
175
+ <Switch size='small' disabled={actionType === 'notify'} />
176
+ </Form.Item>
177
+ <Form.Item
178
+ name='mode'
179
+ label={e('mode')}
180
+ >
181
+ <Select
182
+ style={{ width: 190 }}
183
+ options={triggerModes.map(t => ({ label: t.label, value: t.value }))}
184
+ />
185
+ </Form.Item>
186
+ <Form.Item
187
+ name='cooldownMs'
188
+ label='Cooldown (ms)'
189
+ >
190
+ <InputNumber
191
+ min={0}
192
+ max={600000}
193
+ step={100}
194
+ disabled={mode !== 'cooldown'}
195
+ />
196
+ </Form.Item>
197
+ </div>
198
+ </Form>
199
+ )
200
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Trigger popup opened from the footer icon.
3
+ * Global tab: all predefined triggers, switches change the global on/off
4
+ * default (persisted in db, synced).
5
+ * This-session tab: ALL triggers (predefined + session temp/bookmark rules)
6
+ * with session-only switches (memory, per tab), plus a temp rule editor.
7
+ * Predefined trigger management (create/edit) lives in the settings panel.
8
+ */
9
+ import { auto } from 'manate/react'
10
+ import { Modal, Tabs, Alert, Switch, Button, Empty, Tag } from 'antd'
11
+ import { SettingOutlined } from '@ant-design/icons'
12
+ import TriggerEditor, { matchSummary, actionSummary } from './trigger-editor.jsx'
13
+ import message from '../common/message'
14
+
15
+ const e = window.translate
16
+
17
+ function renderItemWrap (children) {
18
+ return (
19
+ <div
20
+ className='trigger-item pd1x pd1y mg1b'
21
+ style={{ border: '1px solid var(--border, #333)', borderRadius: 4 }}
22
+ >
23
+ {children}
24
+ </div>
25
+ )
26
+ }
27
+
28
+ function renderMeta (t) {
29
+ return (
30
+ <div className='mg1t small muted elli' title={matchSummary(t) + ' → ' + actionSummary(t)}>
31
+ {matchSummary(t)} → {actionSummary(t)}
32
+ </div>
33
+ )
34
+ }
35
+
36
+ export default auto(function TriggerSessionModal (props) {
37
+ const { store } = props
38
+ const open = !!store.triggerSessionOpen
39
+ if (!open) {
40
+ return null
41
+ }
42
+ const tab = store.currentTab
43
+ const tabId = tab?.id || store.activeTabId
44
+ const sessionTriggers = (tab && tab.triggers) || []
45
+ const predefined = store.triggers || []
46
+ const overrides = (tab && tab.triggerOverrides) || {}
47
+
48
+ const handleSessionChange = (next) => {
49
+ const errors = store.setSessionTriggers(tabId, next)
50
+ if (errors && errors.length) {
51
+ message.error(errors[0])
52
+ }
53
+ }
54
+
55
+ const handleClose = () => {
56
+ store.toggleTriggerSessionModal(false)
57
+ }
58
+
59
+ const openManage = () => {
60
+ store.toggleTriggerSessionModal(false)
61
+ store.openTriggers()
62
+ }
63
+
64
+ // global tab: switch = global on/off default (persisted)
65
+ const renderGlobalRow = (t) => {
66
+ return renderItemWrap(
67
+ <div>
68
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
69
+ <Switch
70
+ size='small'
71
+ checked={t.enabled !== false}
72
+ onChange={v => store.editTrigger(t.id, { enabled: v })}
73
+ />
74
+ <b className='elli' style={{ flex: 1 }} title={t.name}>
75
+ {t.name || 'Unnamed'}
76
+ </b>
77
+ <Tag>{t.mode || 'cooldown'}</Tag>
78
+ </div>
79
+ {renderMeta(t)}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ // session tab: switch = on/off for this session only (memory)
85
+ const renderSessionRow = (t) => {
86
+ const globalOn = t.enabled !== false
87
+ const overridden = t.id in overrides
88
+ const checked = overridden ? overrides[t.id] : globalOn
89
+ return renderItemWrap(
90
+ <div>
91
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
92
+ <Switch
93
+ size='small'
94
+ checked={checked}
95
+ onChange={v => store.togglePredefinedTrigger(tabId, t.id, v)}
96
+ />
97
+ <b className='elli' style={{ flex: 1 }} title={t.name}>
98
+ {t.name || 'Unnamed'}
99
+ </b>
100
+ {
101
+ overridden && overrides[t.id] !== globalOn
102
+ ? (
103
+ <span className='small muted'>
104
+ {globalOn ? 'global: on' : 'global: off'}
105
+ </span>
106
+ )
107
+ : null
108
+ }
109
+ <Tag>{t.mode || 'cooldown'}</Tag>
110
+ </div>
111
+ {renderMeta(t)}
112
+ </div>
113
+ )
114
+ }
115
+
116
+ const renderGlobalList = () => {
117
+ return (
118
+ <div>
119
+ <Alert
120
+ type='info'
121
+ showIcon
122
+ className='mg1b'
123
+ message='All predefined triggers. Switches change the global on/off default.'
124
+ />
125
+ {
126
+ !predefined.length
127
+ ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description='No triggers yet — add one in Manage' />
128
+ : predefined.map(renderGlobalRow)
129
+ }
130
+ <div className='pd1t' style={{ textAlign: 'right' }}>
131
+ <Button
132
+ size='small'
133
+ icon={<SettingOutlined />}
134
+ onClick={openManage}
135
+ >
136
+ {e('edit')}
137
+ </Button>
138
+ </div>
139
+ </div>
140
+ )
141
+ }
142
+
143
+ const renderSessionList = () => {
144
+ return (
145
+ <div>
146
+ {
147
+ !predefined.length
148
+ ? null
149
+ : (
150
+ <div className='mg1b'>
151
+ <b>Predefined</b>
152
+ <span className='small muted mg1l'>Switches apply to this session only.</span>
153
+ <div className='pd1t'>
154
+ {predefined.map(renderSessionRow)}
155
+ </div>
156
+ </div>
157
+ )
158
+ }
159
+ <div className='mg1b'>
160
+ <b>This session</b>
161
+ <span className='small muted mg1l'>Temporary rules are not saved to the triggers database.</span>
162
+ </div>
163
+ <TriggerEditor
164
+ value={sessionTriggers}
165
+ onChange={handleSessionChange}
166
+ />
167
+ </div>
168
+ )
169
+ }
170
+
171
+ const activeCount = tab
172
+ ? store.getEffectiveTriggers(tab).length
173
+ : predefined.filter(t => t.enabled !== false).length
174
+
175
+ const items = [
176
+ {
177
+ key: 'predefined',
178
+ label: `${e('global')} (${predefined.length})`,
179
+ children: renderGlobalList()
180
+ },
181
+ {
182
+ key: 'session',
183
+ label: `This session (${activeCount})`,
184
+ children: renderSessionList()
185
+ }
186
+ ]
187
+
188
+ return (
189
+ <Modal
190
+ open={open}
191
+ title={e('triggers')}
192
+ onCancel={handleClose}
193
+ footer={null}
194
+ width={680}
195
+ >
196
+ <Tabs items={items} />
197
+ </Modal>
198
+ )
199
+ })