@goodandready/dsh-clinebot 0.2.0
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/README.ru.md +131 -0
- package/README.zh.md +68 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +895 -0
- package/lib/cline-client.js +342 -0
- package/lib/http.js +34 -0
- package/lib/index.js +573 -0
- package/lib/models.js +210 -0
- package/package.json +68 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
// Dedicated Settings Page: Settings → ClineBot (settings.section)
|
|
2
|
+
// & Plugin Card (settings.plugin.item).
|
|
3
|
+
window.__ModuleLoader__.load({
|
|
4
|
+
id: '@goodandready/dsh-clinebot',
|
|
5
|
+
factory: (require) => {
|
|
6
|
+
var module = { exports: {} }
|
|
7
|
+
const React = require('react')
|
|
8
|
+
const NS = 'dsh-clinebot'
|
|
9
|
+
const TITLE = 'ClineBot'
|
|
10
|
+
const SUBTITLE = 'ClinePass subscription ($9.99/mo) with curated & custom models.'
|
|
11
|
+
const ROUTE_PREFIX = '/dsh-clinebot'
|
|
12
|
+
|
|
13
|
+
function FallbackChevron() {
|
|
14
|
+
return React.createElement(
|
|
15
|
+
'svg',
|
|
16
|
+
{ width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
|
|
17
|
+
React.createElement('path', {
|
|
18
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
19
|
+
stroke: 'currentColor',
|
|
20
|
+
strokeWidth: 1.5,
|
|
21
|
+
strokeLinecap: 'round',
|
|
22
|
+
strokeLinejoin: 'round',
|
|
23
|
+
})
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
const Chevron = FallbackChevron
|
|
27
|
+
|
|
28
|
+
function ensureCss() {
|
|
29
|
+
if (typeof document === 'undefined') return
|
|
30
|
+
if (document.getElementById('dsh-clinebot-full-css')) return
|
|
31
|
+
const style = document.createElement('style')
|
|
32
|
+
style.id = 'dsh-clinebot-full-css'
|
|
33
|
+
style.textContent = `
|
|
34
|
+
.cb-page{display:flex;flex-direction:column;gap:20px;padding:8px 0 32px;max-width:960px}
|
|
35
|
+
.cb-header{display:flex;flex-direction:column;gap:8px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
36
|
+
.cb-page-title{font-size:22px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}
|
|
37
|
+
.cb-page-sub{font-size:14px;color:var(--dsw-alias-label-secondary);line-height:1.5}
|
|
38
|
+
|
|
39
|
+
.cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:14px}
|
|
40
|
+
.cb-section-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
|
|
41
|
+
.cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}
|
|
42
|
+
|
|
43
|
+
.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
|
44
|
+
.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:14px}
|
|
45
|
+
|
|
46
|
+
.cb-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
|
|
47
|
+
.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}
|
|
48
|
+
.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
|
|
49
|
+
.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}
|
|
50
|
+
|
|
51
|
+
.cb-input{height:36px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;width:100%;box-sizing:border-box}
|
|
52
|
+
.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
|
|
53
|
+
.cb-input-group{display:flex;gap:8px;align-items:center}
|
|
54
|
+
|
|
55
|
+
.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px}
|
|
56
|
+
.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
|
|
57
|
+
.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
58
|
+
.cb-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
59
|
+
.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
|
|
60
|
+
.cb-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.1)}
|
|
61
|
+
|
|
62
|
+
.cb-bar-container{display:flex;flex-direction:column;gap:6px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
63
|
+
.cb-bar-head{display:flex;justify-content:space-between;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
64
|
+
.cb-bar-track{width:100%;height:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;border:1px solid var(--dsw-alias-border-l2)}
|
|
65
|
+
.cb-bar-fill{height:100%;border-radius:999px;transition:width .3s}
|
|
66
|
+
.cb-bar-meta{display:flex;justify-content:space-between;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
67
|
+
|
|
68
|
+
.cb-table{width:100%;border-collapse:collapse;margin-top:8px}
|
|
69
|
+
.cb-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
|
|
70
|
+
.cb-table td{padding:10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
|
|
71
|
+
.cb-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
72
|
+
|
|
73
|
+
.cb-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px}
|
|
74
|
+
.cb-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
75
|
+
.cb-preview{padding:12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
|
|
76
|
+
`
|
|
77
|
+
document.head.appendChild(style)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ProgressBar({ label, percentUsed, remainingPercent, resetsAt }) {
|
|
81
|
+
const used = Math.max(0, Math.min(100, percentUsed || 0))
|
|
82
|
+
let fillColor = 'var(--dsw-alias-state-success-primary)'
|
|
83
|
+
if (used > 75) fillColor = 'var(--dsw-alias-state-warning-primary)'
|
|
84
|
+
if (used > 90) fillColor = 'var(--dsw-alias-state-error-primary)'
|
|
85
|
+
|
|
86
|
+
const resetStr = resetsAt ? new Date(resetsAt).toLocaleString() : '—'
|
|
87
|
+
|
|
88
|
+
return React.createElement(
|
|
89
|
+
'div',
|
|
90
|
+
{ className: 'cb-bar-container' },
|
|
91
|
+
React.createElement(
|
|
92
|
+
'div',
|
|
93
|
+
{ className: 'cb-bar-head' },
|
|
94
|
+
React.createElement('span', null, label),
|
|
95
|
+
React.createElement('span', { style: { fontWeight: 600 } }, `${used}% использовано`)
|
|
96
|
+
),
|
|
97
|
+
React.createElement(
|
|
98
|
+
'div',
|
|
99
|
+
{ className: 'cb-bar-track' },
|
|
100
|
+
React.createElement('div', {
|
|
101
|
+
className: 'cb-bar-fill',
|
|
102
|
+
style: { width: `${used}%`, background: fillColor },
|
|
103
|
+
})
|
|
104
|
+
),
|
|
105
|
+
React.createElement(
|
|
106
|
+
'div',
|
|
107
|
+
{ className: 'cb-bar-meta' },
|
|
108
|
+
React.createElement('span', null, `Осталось: ${remainingPercent ?? (100 - used)}%`),
|
|
109
|
+
React.createElement('span', null, `Сброс: ${resetStr}`)
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function SettingsPage() {
|
|
115
|
+
const [status, setStatus] = React.useState(null)
|
|
116
|
+
const [draft, setDraft] = React.useState(null)
|
|
117
|
+
const [busy, setBusy] = React.useState('')
|
|
118
|
+
const [err, setErr] = React.useState('')
|
|
119
|
+
const [msg, setMsg] = React.useState('')
|
|
120
|
+
const [smokeResult, setSmokeResult] = React.useState(null)
|
|
121
|
+
|
|
122
|
+
// Key input state
|
|
123
|
+
const [apiKeyInput, setApiKeyInput] = React.useState('')
|
|
124
|
+
const [showKey, setShowKey] = React.useState(false)
|
|
125
|
+
|
|
126
|
+
// Custom model form state
|
|
127
|
+
const [customId, setCustomId] = React.useState('')
|
|
128
|
+
const [customName, setCustomName] = React.useState('')
|
|
129
|
+
const [customContext, setCustomContext] = React.useState('200000')
|
|
130
|
+
const [customVision, setCustomVision] = React.useState(false)
|
|
131
|
+
const [customCategory, setCustomCategory] = React.useState('coding')
|
|
132
|
+
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
ensureCss()
|
|
135
|
+
}, [])
|
|
136
|
+
|
|
137
|
+
const load = React.useCallback(async () => {
|
|
138
|
+
setErr('')
|
|
139
|
+
const res = await fetch(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
|
|
140
|
+
const data = await res.json().catch(() => ({}))
|
|
141
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
142
|
+
setStatus(data)
|
|
143
|
+
setDraft(data.config || {})
|
|
144
|
+
}, [])
|
|
145
|
+
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
load().catch((e) => setErr(String(e.message || e)))
|
|
148
|
+
}, [load])
|
|
149
|
+
|
|
150
|
+
// Save Key handler
|
|
151
|
+
async function handleSaveKey() {
|
|
152
|
+
if (!apiKeyInput.trim()) {
|
|
153
|
+
setErr('Введите API-ключ перед сохранением')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
setBusy('save-key')
|
|
157
|
+
setErr('')
|
|
158
|
+
setMsg('')
|
|
159
|
+
try {
|
|
160
|
+
const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
apiKey: apiKeyInput.trim(),
|
|
165
|
+
apiKeyEnv: draft?.apiKeyEnv || 'CLINEBOT_API_KEY',
|
|
166
|
+
}),
|
|
167
|
+
})
|
|
168
|
+
const data = await res.json().catch(() => ({}))
|
|
169
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
170
|
+
setMsg(`Ключ сохранён в DSH Credentials (~/.dsh/.credentials.yaml). Валидация: ${data.validated ? 'Успешно' : 'Ошибка'}`)
|
|
171
|
+
setApiKeyInput('')
|
|
172
|
+
await load()
|
|
173
|
+
} catch (e) {
|
|
174
|
+
setErr(String(e.message || e))
|
|
175
|
+
} finally {
|
|
176
|
+
setBusy('')
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Refresh Quota handler
|
|
181
|
+
async function handleRefreshQuota() {
|
|
182
|
+
setBusy('refresh-quota')
|
|
183
|
+
setErr('')
|
|
184
|
+
setMsg('')
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(`${ROUTE_PREFIX}/usage`)
|
|
187
|
+
const data = await res.json().catch(() => ({}))
|
|
188
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
189
|
+
setStatus((prev) => ({ ...prev, usage: data }))
|
|
190
|
+
setMsg('Лимиты подписки обновлены')
|
|
191
|
+
} catch (e) {
|
|
192
|
+
setErr(String(e.message || e))
|
|
193
|
+
} finally {
|
|
194
|
+
setBusy('')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Register/Sync Provider in DSH
|
|
199
|
+
async function handleRegister() {
|
|
200
|
+
setBusy('register')
|
|
201
|
+
setErr('')
|
|
202
|
+
setMsg('')
|
|
203
|
+
try {
|
|
204
|
+
const res = await fetch(`${ROUTE_PREFIX}/register`, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
207
|
+
body: JSON.stringify({ models: draft.enabledModels }),
|
|
208
|
+
})
|
|
209
|
+
const data = await res.json().catch(() => ({}))
|
|
210
|
+
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
211
|
+
setMsg(`Провайдер успешно зарегистрирован в DSH Models (${draft.enabledModels?.length || 0} моделей)`)
|
|
212
|
+
await load()
|
|
213
|
+
} catch (e) {
|
|
214
|
+
setErr(String(e.message || e))
|
|
215
|
+
} finally {
|
|
216
|
+
setBusy('')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Unregister
|
|
221
|
+
async function handleUnregister() {
|
|
222
|
+
setBusy('unregister')
|
|
223
|
+
setErr('')
|
|
224
|
+
setMsg('')
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
|
|
227
|
+
const data = await res.json().catch(() => ({}))
|
|
228
|
+
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
229
|
+
setMsg('Провайдер удален из DSH Models')
|
|
230
|
+
await load()
|
|
231
|
+
} catch (e) {
|
|
232
|
+
setErr(String(e.message || e))
|
|
233
|
+
} finally {
|
|
234
|
+
setBusy('')
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Smoke chat test
|
|
239
|
+
async function handleSmoke() {
|
|
240
|
+
setBusy('smoke')
|
|
241
|
+
setErr('')
|
|
242
|
+
setMsg('')
|
|
243
|
+
setSmokeResult(null)
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ model: draft.defaultModel }),
|
|
249
|
+
})
|
|
250
|
+
const data = await res.json().catch(() => ({}))
|
|
251
|
+
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
252
|
+
setSmokeResult(data)
|
|
253
|
+
setMsg(`Smoke тест пройден! Задержка: ${data.latencyMs} мс`)
|
|
254
|
+
} catch (e) {
|
|
255
|
+
setErr(String(e.message || e))
|
|
256
|
+
} finally {
|
|
257
|
+
setBusy('')
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Toggle model inclusion
|
|
262
|
+
async function handleToggleModel(id) {
|
|
263
|
+
if (!draft) return
|
|
264
|
+
const current = new Set(draft.enabledModels || [])
|
|
265
|
+
if (current.has(id)) {
|
|
266
|
+
current.delete(id)
|
|
267
|
+
} else {
|
|
268
|
+
current.add(id)
|
|
269
|
+
}
|
|
270
|
+
const nextList = Array.from(current)
|
|
271
|
+
setDraft({ ...draft, enabledModels: nextList })
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'Content-Type': 'application/json' },
|
|
277
|
+
body: JSON.stringify({ enabledModels: nextList }),
|
|
278
|
+
})
|
|
279
|
+
} catch {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Select all / filter models
|
|
283
|
+
async function handleSetModelsFilter(type) {
|
|
284
|
+
if (!status?.availableModels) return
|
|
285
|
+
let nextList = []
|
|
286
|
+
if (type === 'all') {
|
|
287
|
+
nextList = status.availableModels.map((m) => m.id)
|
|
288
|
+
} else if (type === 'vision') {
|
|
289
|
+
nextList = status.availableModels.filter((m) => m.input?.includes('vision')).map((m) => m.id)
|
|
290
|
+
} else if (type === 'coding') {
|
|
291
|
+
nextList = status.availableModels.filter((m) => m.category === 'coding').map((m) => m.id)
|
|
292
|
+
} else if (type === 'recommended') {
|
|
293
|
+
nextList = status.availableModels.filter((m) => m.recommended).map((m) => m.id)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
setDraft({ ...draft, enabledModels: nextList })
|
|
297
|
+
try {
|
|
298
|
+
await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
301
|
+
body: JSON.stringify({ enabledModels: nextList }),
|
|
302
|
+
})
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Add custom model
|
|
307
|
+
async function handleAddCustomModel() {
|
|
308
|
+
if (!customId.trim()) {
|
|
309
|
+
setErr('Введите Model ID')
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
setBusy('add-model')
|
|
313
|
+
setErr('')
|
|
314
|
+
try {
|
|
315
|
+
const res = await fetch(`${ROUTE_PREFIX}/models/custom`, {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
id: customId.trim(),
|
|
320
|
+
name: customName.trim() || customId.trim(),
|
|
321
|
+
contextLength: Number(customContext) || 200000,
|
|
322
|
+
hasVision: customVision,
|
|
323
|
+
category: customCategory,
|
|
324
|
+
}),
|
|
325
|
+
})
|
|
326
|
+
const data = await res.json().catch(() => ({}))
|
|
327
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
328
|
+
setMsg(`Модель ${data.model?.name} успешно добавлена в каталог`)
|
|
329
|
+
setCustomId('')
|
|
330
|
+
setCustomName('')
|
|
331
|
+
await load()
|
|
332
|
+
} catch (e) {
|
|
333
|
+
setErr(String(e.message || e))
|
|
334
|
+
} finally {
|
|
335
|
+
setBusy('')
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Remove custom model
|
|
340
|
+
async function handleRemoveCustomModel(id) {
|
|
341
|
+
setBusy(`remove-${id}`)
|
|
342
|
+
setErr('')
|
|
343
|
+
try {
|
|
344
|
+
const res = await fetch(`${ROUTE_PREFIX}/models/custom`, {
|
|
345
|
+
method: 'DELETE',
|
|
346
|
+
headers: { 'Content-Type': 'application/json' },
|
|
347
|
+
body: JSON.stringify({ id }),
|
|
348
|
+
})
|
|
349
|
+
const data = await res.json().catch(() => ({}))
|
|
350
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
351
|
+
setMsg(`Модель ${id} удалена`)
|
|
352
|
+
await load()
|
|
353
|
+
} catch (e) {
|
|
354
|
+
setErr(String(e.message || e))
|
|
355
|
+
} finally {
|
|
356
|
+
setBusy('')
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!status || !draft) {
|
|
361
|
+
if (err) {
|
|
362
|
+
return React.createElement(
|
|
363
|
+
'div',
|
|
364
|
+
{ className: 'cb-page' },
|
|
365
|
+
React.createElement('div', { className: 'cb-alert cb-alert-err' }, `Ошибка загрузки настроек ClineBot: ${err}`),
|
|
366
|
+
React.createElement(
|
|
367
|
+
'button',
|
|
368
|
+
{
|
|
369
|
+
type: 'button',
|
|
370
|
+
className: 'cb-btn cb-btn-secondary',
|
|
371
|
+
style: { marginTop: '12px', alignSelf: 'flex-start' },
|
|
372
|
+
onClick: () => {
|
|
373
|
+
setErr('')
|
|
374
|
+
load().catch((e) => setErr(String(e.message || e)))
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
'Повторить попытку'
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
return React.createElement('div', { className: 'cb-page' }, 'Загрузка настроек ClineBot…')
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const healthOk = !!status.health?.ok
|
|
385
|
+
const keyPresent = !!status.key?.present
|
|
386
|
+
const isRegistered = !!status.isRegistered
|
|
387
|
+
const modelsList = status.availableModels || []
|
|
388
|
+
const enabledSet = new Set(draft.enabledModels || [])
|
|
389
|
+
const usage = status.usage
|
|
390
|
+
|
|
391
|
+
return React.createElement(
|
|
392
|
+
'div',
|
|
393
|
+
{ className: 'cb-page' },
|
|
394
|
+
|
|
395
|
+
// Page Header
|
|
396
|
+
React.createElement(
|
|
397
|
+
'div',
|
|
398
|
+
{ className: 'cb-header' },
|
|
399
|
+
React.createElement(
|
|
400
|
+
'div',
|
|
401
|
+
{ className: 'cb-page-title' },
|
|
402
|
+
'🤖 ClineBot (ClinePass) Provider',
|
|
403
|
+
React.createElement(
|
|
404
|
+
'span',
|
|
405
|
+
{ className: `cb-badge ${healthOk ? 'cb-badge-ok' : 'cb-badge-bad'}` },
|
|
406
|
+
healthOk ? `Сервер доступен (${status.health?.latencyMs} мс)` : 'Недоступен'
|
|
407
|
+
),
|
|
408
|
+
React.createElement(
|
|
409
|
+
'span',
|
|
410
|
+
{ className: `cb-badge ${keyPresent ? 'cb-badge-ok' : 'cb-badge-warn'}` },
|
|
411
|
+
keyPresent ? `Ключ ✓ (${status.key?.source})` : 'Ключ отсутствует'
|
|
412
|
+
),
|
|
413
|
+
React.createElement(
|
|
414
|
+
'span',
|
|
415
|
+
{ className: `cb-badge ${isRegistered ? 'cb-badge-ok' : 'cb-badge-warn'}` },
|
|
416
|
+
isRegistered ? 'Зарегистрирован в DSH' : 'Не зарегистрирован'
|
|
417
|
+
)
|
|
418
|
+
),
|
|
419
|
+
React.createElement(
|
|
420
|
+
'div',
|
|
421
|
+
{ className: 'cb-page-sub' },
|
|
422
|
+
'Подключение моделей по подписке ClinePass ($9.99/мес). Провайдер совместим с OpenAI API, автоматически отслеживает скользящие лимиты и регистрирует проверенный каталог моделей в DSH.'
|
|
423
|
+
)
|
|
424
|
+
),
|
|
425
|
+
|
|
426
|
+
// Notifications
|
|
427
|
+
err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
|
|
428
|
+
msg ? React.createElement('div', { className: 'cb-alert-ok' }, msg) : null,
|
|
429
|
+
|
|
430
|
+
// Card 1: API Key and Credentials
|
|
431
|
+
React.createElement(
|
|
432
|
+
'div',
|
|
433
|
+
{ className: 'cb-section-card' },
|
|
434
|
+
React.createElement(
|
|
435
|
+
'div',
|
|
436
|
+
{ className: 'cb-section-title' },
|
|
437
|
+
'🔑 Авторизация и API-ключ'
|
|
438
|
+
),
|
|
439
|
+
React.createElement(
|
|
440
|
+
'div',
|
|
441
|
+
{ className: 'cb-section-desc' },
|
|
442
|
+
'Ключ сохраняется в системное хранилище DSH Credentials (~/.dsh/.credentials.yaml) и никогда не попадает в открытые файлы настроек.'
|
|
443
|
+
),
|
|
444
|
+
React.createElement(
|
|
445
|
+
'div',
|
|
446
|
+
{ className: 'cb-input-group' },
|
|
447
|
+
React.createElement('input', {
|
|
448
|
+
className: 'cb-input',
|
|
449
|
+
type: showKey ? 'text' : 'password',
|
|
450
|
+
placeholder: keyPresent ? '••••••••••••••••••••••••' : 'Вставьте ключ ClinePass (cline_...)',
|
|
451
|
+
value: apiKeyInput,
|
|
452
|
+
onChange: (e) => setApiKeyInput(e.target.value),
|
|
453
|
+
}),
|
|
454
|
+
React.createElement(
|
|
455
|
+
'button',
|
|
456
|
+
{
|
|
457
|
+
type: 'button',
|
|
458
|
+
className: 'cb-btn',
|
|
459
|
+
onClick: () => setShowKey((v) => !v),
|
|
460
|
+
},
|
|
461
|
+
showKey ? 'Скрыть' : 'Показать'
|
|
462
|
+
),
|
|
463
|
+
React.createElement(
|
|
464
|
+
'button',
|
|
465
|
+
{
|
|
466
|
+
type: 'button',
|
|
467
|
+
className: 'cb-btn cb-btn-primary',
|
|
468
|
+
disabled: !!busy || !apiKeyInput.trim(),
|
|
469
|
+
onClick: handleSaveKey,
|
|
470
|
+
},
|
|
471
|
+
busy === 'save-key' ? 'Сохранение…' : 'Сохранить ключ'
|
|
472
|
+
)
|
|
473
|
+
),
|
|
474
|
+
React.createElement(
|
|
475
|
+
'div',
|
|
476
|
+
{ className: 'cb-row', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
477
|
+
React.createElement('span', null, `Переменная учётных данных: `),
|
|
478
|
+
React.createElement('code', null, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
|
|
479
|
+
React.createElement('span', null, ` · `),
|
|
480
|
+
React.createElement(
|
|
481
|
+
'a',
|
|
482
|
+
{
|
|
483
|
+
href: 'https://app.cline.bot',
|
|
484
|
+
target: '_blank',
|
|
485
|
+
rel: 'noreferrer',
|
|
486
|
+
style: { color: 'var(--dsw-alias-state-brand-primary)' },
|
|
487
|
+
},
|
|
488
|
+
'Получить ключ в консоли app.cline.bot ↗'
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
),
|
|
492
|
+
|
|
493
|
+
// Card 2: Subscription Limits & Usage Dashboard
|
|
494
|
+
keyPresent
|
|
495
|
+
? React.createElement(
|
|
496
|
+
'div',
|
|
497
|
+
{ className: 'cb-section-card' },
|
|
498
|
+
React.createElement(
|
|
499
|
+
'div',
|
|
500
|
+
{ className: 'cb-section-title' },
|
|
501
|
+
'📊 Остаток лимитов подписки (ClinePass Quota)',
|
|
502
|
+
React.createElement(
|
|
503
|
+
'button',
|
|
504
|
+
{
|
|
505
|
+
type: 'button',
|
|
506
|
+
className: 'cb-btn',
|
|
507
|
+
disabled: !!busy,
|
|
508
|
+
onClick: handleRefreshQuota,
|
|
509
|
+
},
|
|
510
|
+
busy === 'refresh-quota' ? 'Обновление…' : 'Обновить квоты'
|
|
511
|
+
)
|
|
512
|
+
),
|
|
513
|
+
React.createElement(
|
|
514
|
+
'div',
|
|
515
|
+
{ className: 'cb-section-desc' },
|
|
516
|
+
usage?.user?.email
|
|
517
|
+
? `Аккаунт: ${usage.user.email} · Тариф: ${usage.plan || 'ClinePass ($9.99/mo)'}`
|
|
518
|
+
: 'Официальные лимиты скользящих окон запросов ClinePass'
|
|
519
|
+
),
|
|
520
|
+
React.createElement(
|
|
521
|
+
'div',
|
|
522
|
+
{ className: 'cb-grid-2' },
|
|
523
|
+
React.createElement(ProgressBar, {
|
|
524
|
+
label: '⏱ 5-часовое скользящее окно (Rolling Limit)',
|
|
525
|
+
percentUsed: usage?.windows?.fiveHour?.percentUsed || 0,
|
|
526
|
+
remainingPercent: usage?.windows?.fiveHour?.remainingPercent || 100,
|
|
527
|
+
resetsAt: usage?.windows?.fiveHour?.resetsAt,
|
|
528
|
+
}),
|
|
529
|
+
React.createElement(ProgressBar, {
|
|
530
|
+
label: '📅 Недельное окно (Weekly Window)',
|
|
531
|
+
percentUsed: usage?.windows?.weekly?.percentUsed || 0,
|
|
532
|
+
remainingPercent: usage?.windows?.weekly?.remainingPercent || 100,
|
|
533
|
+
resetsAt: usage?.windows?.weekly?.resetsAt,
|
|
534
|
+
})
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
: null,
|
|
538
|
+
|
|
539
|
+
// Card 3: Model Picker Management
|
|
540
|
+
React.createElement(
|
|
541
|
+
'div',
|
|
542
|
+
{ className: 'cb-section-card' },
|
|
543
|
+
React.createElement(
|
|
544
|
+
'div',
|
|
545
|
+
{ className: 'cb-section-title' },
|
|
546
|
+
`🎯 Модели в пикере DSH (${enabledSet.size} из ${modelsList.length} включено)`,
|
|
547
|
+
React.createElement(
|
|
548
|
+
'div',
|
|
549
|
+
{ className: 'cb-row' },
|
|
550
|
+
React.createElement(
|
|
551
|
+
'button',
|
|
552
|
+
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') },
|
|
553
|
+
'Все'
|
|
554
|
+
),
|
|
555
|
+
React.createElement(
|
|
556
|
+
'button',
|
|
557
|
+
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('vision') },
|
|
558
|
+
'Только Vision'
|
|
559
|
+
),
|
|
560
|
+
React.createElement(
|
|
561
|
+
'button',
|
|
562
|
+
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('coding') },
|
|
563
|
+
'Только Кодинг'
|
|
564
|
+
),
|
|
565
|
+
React.createElement(
|
|
566
|
+
'button',
|
|
567
|
+
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('recommended') },
|
|
568
|
+
'Рекомендованные'
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
),
|
|
572
|
+
React.createElement(
|
|
573
|
+
'div',
|
|
574
|
+
{ className: 'cb-section-desc' },
|
|
575
|
+
'Отметьте галочками модели, которые должны отображаться в выпадающем списке выбора моделей в чате DeepSeek Harness.'
|
|
576
|
+
),
|
|
577
|
+
React.createElement(
|
|
578
|
+
'table',
|
|
579
|
+
{ className: 'cb-table' },
|
|
580
|
+
React.createElement(
|
|
581
|
+
'thead',
|
|
582
|
+
null,
|
|
583
|
+
React.createElement(
|
|
584
|
+
'tr',
|
|
585
|
+
null,
|
|
586
|
+
React.createElement('th', { style: { width: '40px' } }, 'Вкл'),
|
|
587
|
+
React.createElement('th', null, 'Название модели'),
|
|
588
|
+
React.createElement('th', null, 'Model ID'),
|
|
589
|
+
React.createElement('th', null, 'Контекст'),
|
|
590
|
+
React.createElement('th', null, 'Возможности')
|
|
591
|
+
)
|
|
592
|
+
),
|
|
593
|
+
React.createElement(
|
|
594
|
+
'tbody',
|
|
595
|
+
null,
|
|
596
|
+
modelsList.map((m) =>
|
|
597
|
+
React.createElement(
|
|
598
|
+
'tr',
|
|
599
|
+
{ key: m.id },
|
|
600
|
+
React.createElement(
|
|
601
|
+
'td',
|
|
602
|
+
null,
|
|
603
|
+
React.createElement('input', {
|
|
604
|
+
type: 'checkbox',
|
|
605
|
+
checked: enabledSet.has(m.id),
|
|
606
|
+
onChange: () => handleToggleModel(m.id),
|
|
607
|
+
})
|
|
608
|
+
),
|
|
609
|
+
React.createElement(
|
|
610
|
+
'td',
|
|
611
|
+
null,
|
|
612
|
+
React.createElement('strong', null, m.name),
|
|
613
|
+
m.recommended
|
|
614
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, 'Star')
|
|
615
|
+
: null,
|
|
616
|
+
m.isCustom
|
|
617
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-warn', style: { marginLeft: '6px' } }, 'Custom')
|
|
618
|
+
: null
|
|
619
|
+
),
|
|
620
|
+
React.createElement('td', null, React.createElement('code', null, m.id)),
|
|
621
|
+
React.createElement('td', null, `${Math.round((m.contextLength || 200000) / 1000)}k`),
|
|
622
|
+
React.createElement(
|
|
623
|
+
'td',
|
|
624
|
+
null,
|
|
625
|
+
React.createElement('span', { className: 'cb-badge' }, m.category || 'general'),
|
|
626
|
+
m.input?.includes('vision')
|
|
627
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
|
|
628
|
+
: null
|
|
629
|
+
)
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
),
|
|
635
|
+
|
|
636
|
+
// Card 4: Add New Custom Model Form
|
|
637
|
+
React.createElement(
|
|
638
|
+
'div',
|
|
639
|
+
{ className: 'cb-section-card' },
|
|
640
|
+
React.createElement(
|
|
641
|
+
'div',
|
|
642
|
+
{ className: 'cb-section-title' },
|
|
643
|
+
'➕ Добавить новую модель в подписку'
|
|
644
|
+
),
|
|
645
|
+
React.createElement(
|
|
646
|
+
'div',
|
|
647
|
+
{ className: 'cb-section-desc' },
|
|
648
|
+
'Если в сервисе ClinePass появилась новая модель, добавьте её идентификатор сюда — она мгновенно станет доступна в DSH без обновления плагина.'
|
|
649
|
+
),
|
|
650
|
+
React.createElement(
|
|
651
|
+
'div',
|
|
652
|
+
{ className: 'cb-grid-2' },
|
|
653
|
+
React.createElement(
|
|
654
|
+
'div',
|
|
655
|
+
null,
|
|
656
|
+
React.createElement('label', { className: 'cb-section-desc' }, 'Model ID (напр. cline-pass/deepseek-v4-moe)'),
|
|
657
|
+
React.createElement('input', {
|
|
658
|
+
className: 'cb-input',
|
|
659
|
+
placeholder: 'cline-pass/...',
|
|
660
|
+
value: customId,
|
|
661
|
+
onChange: (e) => setCustomId(e.target.value),
|
|
662
|
+
})
|
|
663
|
+
),
|
|
664
|
+
React.createElement(
|
|
665
|
+
'div',
|
|
666
|
+
null,
|
|
667
|
+
React.createElement('label', { className: 'cb-section-desc' }, 'Отображаемое имя (Display Name)'),
|
|
668
|
+
React.createElement('input', {
|
|
669
|
+
className: 'cb-input',
|
|
670
|
+
placeholder: 'Например: DeepSeek V4 MoE',
|
|
671
|
+
value: customName,
|
|
672
|
+
onChange: (e) => setCustomName(e.target.value),
|
|
673
|
+
})
|
|
674
|
+
),
|
|
675
|
+
React.createElement(
|
|
676
|
+
'div',
|
|
677
|
+
null,
|
|
678
|
+
React.createElement('label', { className: 'cb-section-desc' }, 'Контекст (токенов)'),
|
|
679
|
+
React.createElement('input', {
|
|
680
|
+
className: 'cb-input',
|
|
681
|
+
type: 'number',
|
|
682
|
+
value: customContext,
|
|
683
|
+
onChange: (e) => setCustomContext(e.target.value),
|
|
684
|
+
})
|
|
685
|
+
),
|
|
686
|
+
React.createElement(
|
|
687
|
+
'div',
|
|
688
|
+
null,
|
|
689
|
+
React.createElement('label', { className: 'cb-section-desc' }, 'Категория'),
|
|
690
|
+
React.createElement(
|
|
691
|
+
'select',
|
|
692
|
+
{
|
|
693
|
+
className: 'cb-input',
|
|
694
|
+
value: customCategory,
|
|
695
|
+
onChange: (e) => setCustomCategory(e.target.value),
|
|
696
|
+
},
|
|
697
|
+
React.createElement('option', { value: 'coding' }, 'Кодинг (Coding)'),
|
|
698
|
+
React.createElement('option', { value: 'reasoning' }, 'Рассуждения (Reasoning)'),
|
|
699
|
+
React.createElement('option', { value: 'multimodal' }, 'Мультимодальная'),
|
|
700
|
+
React.createElement('option', { value: 'general' }, 'Общая (General)')
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
),
|
|
704
|
+
React.createElement(
|
|
705
|
+
'div',
|
|
706
|
+
{ className: 'cb-row', style: { marginTop: '4px' } },
|
|
707
|
+
React.createElement(
|
|
708
|
+
'label',
|
|
709
|
+
{ className: 'cb-row', style: { cursor: 'pointer', fontSize: '13px' } },
|
|
710
|
+
React.createElement('input', {
|
|
711
|
+
type: 'checkbox',
|
|
712
|
+
checked: customVision,
|
|
713
|
+
onChange: (e) => setCustomVision(e.target.checked),
|
|
714
|
+
}),
|
|
715
|
+
' Модель поддерживает изображения (Vision)'
|
|
716
|
+
),
|
|
717
|
+
React.createElement(
|
|
718
|
+
'button',
|
|
719
|
+
{
|
|
720
|
+
type: 'button',
|
|
721
|
+
className: 'cb-btn cb-btn-primary',
|
|
722
|
+
style: { marginLeft: 'auto' },
|
|
723
|
+
disabled: !!busy || !customId.trim(),
|
|
724
|
+
onClick: handleAddCustomModel,
|
|
725
|
+
},
|
|
726
|
+
busy === 'add-model' ? 'Добавление…' : 'Добавить модель'
|
|
727
|
+
)
|
|
728
|
+
),
|
|
729
|
+
// Custom models list with remove button
|
|
730
|
+
draft.customModels?.length > 0
|
|
731
|
+
? React.createElement(
|
|
732
|
+
'div',
|
|
733
|
+
{ style: { marginTop: '10px' } },
|
|
734
|
+
React.createElement('div', { className: 'cb-section-desc' }, 'Добавленные вами модели:'),
|
|
735
|
+
React.createElement(
|
|
736
|
+
'div',
|
|
737
|
+
{ className: 'cb-row', style: { marginTop: '6px' } },
|
|
738
|
+
draft.customModels.map((cm) =>
|
|
739
|
+
React.createElement(
|
|
740
|
+
'span',
|
|
741
|
+
{ key: cm.id, className: 'cb-badge' },
|
|
742
|
+
`${cm.name} (${cm.id})`,
|
|
743
|
+
React.createElement(
|
|
744
|
+
'button',
|
|
745
|
+
{
|
|
746
|
+
type: 'button',
|
|
747
|
+
style: {
|
|
748
|
+
background: 'none',
|
|
749
|
+
border: 'none',
|
|
750
|
+
cursor: 'pointer',
|
|
751
|
+
color: 'var(--dsw-alias-state-error-primary)',
|
|
752
|
+
padding: 0,
|
|
753
|
+
marginLeft: '4px',
|
|
754
|
+
},
|
|
755
|
+
onClick: () => handleRemoveCustomModel(cm.id),
|
|
756
|
+
},
|
|
757
|
+
'×'
|
|
758
|
+
)
|
|
759
|
+
)
|
|
760
|
+
)
|
|
761
|
+
)
|
|
762
|
+
)
|
|
763
|
+
: null
|
|
764
|
+
),
|
|
765
|
+
|
|
766
|
+
// Card 5: Diagnostics & Sync with DSH
|
|
767
|
+
React.createElement(
|
|
768
|
+
'div',
|
|
769
|
+
{ className: 'cb-section-card' },
|
|
770
|
+
React.createElement(
|
|
771
|
+
'div',
|
|
772
|
+
{ className: 'cb-section-title' },
|
|
773
|
+
'⚡ Диагностика и регистрация в DSH Models'
|
|
774
|
+
),
|
|
775
|
+
React.createElement(
|
|
776
|
+
'div',
|
|
777
|
+
{ className: 'cb-section-desc' },
|
|
778
|
+
'Проверьте связь с эндпоинтом и синхронизируйте активный список моделей с ядром DeepSeek Harness.'
|
|
779
|
+
),
|
|
780
|
+
React.createElement(
|
|
781
|
+
'div',
|
|
782
|
+
{ className: 'cb-row' },
|
|
783
|
+
React.createElement(
|
|
784
|
+
'button',
|
|
785
|
+
{
|
|
786
|
+
type: 'button',
|
|
787
|
+
className: 'cb-btn',
|
|
788
|
+
disabled: !!busy || !keyPresent,
|
|
789
|
+
onClick: handleSmoke,
|
|
790
|
+
},
|
|
791
|
+
busy === 'smoke' ? 'Тестирование…' : 'Запустить Smoke Test (Ping)'
|
|
792
|
+
),
|
|
793
|
+
isRegistered
|
|
794
|
+
? React.createElement(
|
|
795
|
+
'button',
|
|
796
|
+
{
|
|
797
|
+
type: 'button',
|
|
798
|
+
className: 'cb-btn cb-btn-danger',
|
|
799
|
+
disabled: !!busy,
|
|
800
|
+
onClick: handleUnregister,
|
|
801
|
+
},
|
|
802
|
+
busy === 'unregister' ? 'Удаление…' : 'Удалить из DSH Models'
|
|
803
|
+
)
|
|
804
|
+
: null,
|
|
805
|
+
React.createElement(
|
|
806
|
+
'button',
|
|
807
|
+
{
|
|
808
|
+
type: 'button',
|
|
809
|
+
className: 'cb-btn cb-btn-primary',
|
|
810
|
+
style: { marginLeft: 'auto' },
|
|
811
|
+
disabled: !!busy,
|
|
812
|
+
onClick: handleRegister,
|
|
813
|
+
},
|
|
814
|
+
busy === 'register' ? 'Синхронизация…' : 'Синхронизировать с DSH Models'
|
|
815
|
+
)
|
|
816
|
+
),
|
|
817
|
+
smokeResult
|
|
818
|
+
? React.createElement(
|
|
819
|
+
'div',
|
|
820
|
+
{ className: 'cb-preview' },
|
|
821
|
+
`✅ Задержка ответа: ${smokeResult.latencyMs} мс | Модель: ${smokeResult.model}\nОтвет: ${smokeResult.preview || '(пустой ответ)'}`
|
|
822
|
+
)
|
|
823
|
+
: null
|
|
824
|
+
)
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Accordion item for Plugins section
|
|
829
|
+
function PluginCard() {
|
|
830
|
+
const [open, setOpen] = React.useState(false)
|
|
831
|
+
React.useEffect(() => {
|
|
832
|
+
ensureCss()
|
|
833
|
+
}, [])
|
|
834
|
+
|
|
835
|
+
return React.createElement(
|
|
836
|
+
'li',
|
|
837
|
+
{ className: 'cb-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
|
|
838
|
+
React.createElement(
|
|
839
|
+
'button',
|
|
840
|
+
{
|
|
841
|
+
type: 'button',
|
|
842
|
+
style: {
|
|
843
|
+
background: 'none',
|
|
844
|
+
border: 'none',
|
|
845
|
+
cursor: 'pointer',
|
|
846
|
+
display: 'flex',
|
|
847
|
+
alignItems: 'center',
|
|
848
|
+
width: '100%',
|
|
849
|
+
padding: 0,
|
|
850
|
+
textAlign: 'left',
|
|
851
|
+
},
|
|
852
|
+
'aria-expanded': open,
|
|
853
|
+
onClick: () => setOpen((v) => !v),
|
|
854
|
+
},
|
|
855
|
+
React.createElement(
|
|
856
|
+
'div',
|
|
857
|
+
{ style: { flex: 1 } },
|
|
858
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, TITLE),
|
|
859
|
+
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, SUBTITLE)
|
|
860
|
+
),
|
|
861
|
+
React.createElement('span', { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
862
|
+
React.createElement(Chevron)
|
|
863
|
+
)
|
|
864
|
+
),
|
|
865
|
+
open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage)) : null
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function apply(ctx) {
|
|
870
|
+
// 1. Register as dedicated Settings Section (Settings -> ClineBot)
|
|
871
|
+
ctx.slots.inject('settings.section', () => {
|
|
872
|
+
return ctx.slots.register(
|
|
873
|
+
{
|
|
874
|
+
name: 'settings.section',
|
|
875
|
+
id: '@goodandready/dsh-clinebot',
|
|
876
|
+
order: 28,
|
|
877
|
+
label: () => TITLE,
|
|
878
|
+
},
|
|
879
|
+
SettingsPage
|
|
880
|
+
)
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
// 2. Also register in settings.plugin.item (Settings -> Plugins)
|
|
884
|
+
ctx.slots.inject('settings.plugin.item', () => {
|
|
885
|
+
return ctx.slots.register(
|
|
886
|
+
{ name: 'settings.plugin.item', key: NS },
|
|
887
|
+
PluginCard
|
|
888
|
+
)
|
|
889
|
+
})
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
module.exports = { apply, inject: ['slots'] }
|
|
893
|
+
return module.exports
|
|
894
|
+
},
|
|
895
|
+
})
|