@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/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
+ })