@goodandready/dsh-clinebot 0.2.1 → 0.3.1

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 CHANGED
@@ -10,6 +10,32 @@ window.__ModuleLoader__.load({
10
10
  const SUBTITLE = 'ClinePass subscription ($9.99/mo) with curated & custom models.'
11
11
  const ROUTE_PREFIX = '/dsh-clinebot'
12
12
 
13
+ const en = {
14
+ title: 'ClineBot',
15
+ subtitle: 'ClinePass subscription ($9.99/mo) with curated & custom models.',
16
+ 'settings.loading': 'Loading ClineBot settings…',
17
+ 'settings.unavailable': 'ClineBot settings unavailable (host namespace is not ready).',
18
+ }
19
+
20
+ const ru = {
21
+ title: 'ClineBot',
22
+ subtitle: 'Подписка ClinePass ($9.99/мес) с каталогом проверенных моделей.',
23
+ 'settings.loading': 'Загрузка настроек ClineBot…',
24
+ 'settings.unavailable': 'Настройки ClineBot недоступны (пространство хоста ещё не готово).',
25
+ }
26
+
27
+ function makeT(dict, fallback) {
28
+ return function t(key, vars) {
29
+ let val = (dict && dict[key]) || (fallback && fallback[key]) || key
30
+ if (vars && typeof val === 'string') {
31
+ for (const k of Object.keys(vars)) {
32
+ val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
33
+ }
34
+ }
35
+ return val
36
+ }
37
+ }
38
+
13
39
  function FallbackChevron() {
14
40
  return React.createElement(
15
41
  'svg',
@@ -30,6 +56,7 @@ window.__ModuleLoader__.load({
30
56
  if (document.getElementById('dsh-clinebot-full-css')) return
31
57
  const style = document.createElement('style')
32
58
  style.id = 'dsh-clinebot-full-css'
59
+ style.dataset.dshPlugin = NS
33
60
  style.textContent = `
34
61
  .cb-page{display:flex;flex-direction:column;gap:20px;padding:8px 0 32px;max-width:960px}
35
62
  .cb-header{display:flex;flex-direction:column;gap:8px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}
@@ -52,12 +79,13 @@ window.__ModuleLoader__.load({
52
79
  .cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
53
80
  .cb-input-group{display:flex;gap:8px;align-items:center}
54
81
 
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)}
82
+ .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;transition:all .15s ease}
83
+ .cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
57
84
  .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}
85
+ .cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88;visibility:visible !important}
59
86
  .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)}
87
+ .cb-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12) !important;border-color:rgba(239,68,68,0.5)}
88
+ .cb-btn:disabled{opacity:0.5;cursor:not-allowed}
61
89
 
62
90
  .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
91
  .cb-bar-head{display:flex;justify-content:space-between;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
@@ -72,6 +100,12 @@ window.__ModuleLoader__.load({
72
100
 
73
101
  .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
102
  .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}
103
+ .cb-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
104
+ .cb-banner-warning{padding:12px 16px;border-radius:8px;background:rgba(245,158,11,0.12);border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
105
+ .cb-banner-exhausted{padding:12px 16px;border-radius:8px;background:rgba(239,68,68,0.12);border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:600}
106
+ .cb-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
107
+ .cb-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
108
+ .cb-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
75
109
  .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
110
  `
77
111
  document.head.appendChild(style)
@@ -111,7 +145,22 @@ window.__ModuleLoader__.load({
111
145
  )
112
146
  }
113
147
 
114
- function SettingsPage() {
148
+ function SettingsPage(props) {
149
+ const ctx = props?.ctx
150
+ const t = props?.t || makeT(ru, en)
151
+
152
+ const scope = React.useMemo(
153
+ () => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
154
+ [ctx]
155
+ )
156
+
157
+ const snapshot = React.useSyncExternalStore(
158
+ React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
159
+ React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'ready' }), [scope]),
160
+ React.useCallback(() => ({ status: 'loading' }), [])
161
+ )
162
+ const snapshotStatus = snapshot?.status || 'ready'
163
+
115
164
  const [status, setStatus] = React.useState(null)
116
165
  const [draft, setDraft] = React.useState(null)
117
166
  const [busy, setBusy] = React.useState('')
@@ -123,13 +172,6 @@ window.__ModuleLoader__.load({
123
172
  const [apiKeyInput, setApiKeyInput] = React.useState('')
124
173
  const [showKey, setShowKey] = React.useState(false)
125
174
 
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
175
  React.useEffect(() => {
134
176
  ensureCss()
135
177
  }, [])
@@ -195,6 +237,24 @@ window.__ModuleLoader__.load({
195
237
  }
196
238
  }
197
239
 
240
+ // Sync official models from ClinePass plan
241
+ async function handleSyncPlanModels() {
242
+ setBusy('sync-models')
243
+ setErr('')
244
+ setMsg('')
245
+ try {
246
+ const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
247
+ const data = await res.json().catch(() => ({}))
248
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
249
+ setMsg(`Синхронизировано моделей подписки: ${data.totalModelsCount} (${data.discoveredCount} получено напрямую из плана ClinePass)`)
250
+ await load()
251
+ } catch (e) {
252
+ setErr(String(e.message || e))
253
+ } finally {
254
+ setBusy('')
255
+ }
256
+ }
257
+
198
258
  // Register/Sync Provider in DSH
199
259
  async function handleRegister() {
200
260
  setBusy('register')
@@ -251,6 +311,7 @@ window.__ModuleLoader__.load({
251
311
  if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
252
312
  setSmokeResult(data)
253
313
  setMsg(`Smoke тест пройден! Задержка: ${data.latencyMs} мс`)
314
+ await load()
254
315
  } catch (e) {
255
316
  setErr(String(e.message || e))
256
317
  } finally {
@@ -270,6 +331,9 @@ window.__ModuleLoader__.load({
270
331
  const nextList = Array.from(current)
271
332
  setDraft({ ...draft, enabledModels: nextList })
272
333
 
334
+ if (scope && snapshotStatus === 'ready') {
335
+ try { await scope.set('enabledModels', nextList) } catch (_) {}
336
+ }
273
337
  try {
274
338
  await fetch(`${ROUTE_PREFIX}/models/toggle`, {
275
339
  method: 'POST',
@@ -286,7 +350,7 @@ window.__ModuleLoader__.load({
286
350
  if (type === 'all') {
287
351
  nextList = status.availableModels.map((m) => m.id)
288
352
  } else if (type === 'vision') {
289
- nextList = status.availableModels.filter((m) => m.input?.includes('vision')).map((m) => m.id)
353
+ nextList = status.availableModels.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id)
290
354
  } else if (type === 'coding') {
291
355
  nextList = status.availableModels.filter((m) => m.category === 'coding').map((m) => m.id)
292
356
  } else if (type === 'recommended') {
@@ -294,6 +358,9 @@ window.__ModuleLoader__.load({
294
358
  }
295
359
 
296
360
  setDraft({ ...draft, enabledModels: nextList })
361
+ if (scope && snapshotStatus === 'ready') {
362
+ try { await scope.set('enabledModels', nextList) } catch (_) {}
363
+ }
297
364
  try {
298
365
  await fetch(`${ROUTE_PREFIX}/models/toggle`, {
299
366
  method: 'POST',
@@ -303,61 +370,15 @@ window.__ModuleLoader__.load({
303
370
  } catch {}
304
371
  }
305
372
 
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
- }
373
+ if (snapshotStatus === 'unavailable') {
374
+ return React.createElement(
375
+ 'div',
376
+ { className: 'cb-page' },
377
+ React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
378
+ )
358
379
  }
359
380
 
360
- if (!status || !draft) {
381
+ if (!status || !draft || snapshotStatus === 'loading') {
361
382
  if (err) {
362
383
  return React.createElement(
363
384
  'div',
@@ -490,6 +511,27 @@ window.__ModuleLoader__.load({
490
511
  )
491
512
  ),
492
513
 
514
+ // Quota Warning Banner
515
+ status.quotaWarning
516
+ ? React.createElement(
517
+ 'div',
518
+ { className: status.quotaWarning.level === 'exhausted' ? 'cb-banner-exhausted' : 'cb-banner-warning' },
519
+ React.createElement('span', { style: { fontSize: '16px' } }, status.quotaWarning.level === 'exhausted' ? '🚨' : '⚠️'),
520
+ React.createElement(
521
+ 'div',
522
+ { style: { flex: 1 } },
523
+ React.createElement('div', null, status.quotaWarning.message),
524
+ status.quotaWarning.resetsAt
525
+ ? React.createElement(
526
+ 'div',
527
+ { style: { fontSize: '11px', opacity: 0.9, marginTop: '2px' } },
528
+ `Ожидаемое время сброса лимита: ${new Date(status.quotaWarning.resetsAt).toLocaleTimeString()}`
529
+ )
530
+ : null
531
+ )
532
+ )
533
+ : null,
534
+
493
535
  // Card 2: Subscription Limits & Usage Dashboard
494
536
  keyPresent
495
537
  ? React.createElement(
@@ -536,7 +578,7 @@ window.__ModuleLoader__.load({
536
578
  )
537
579
  : null,
538
580
 
539
- // Card 3: Model Picker Management
581
+ // Card 3: Model Picker Management with Official Plan Sync
540
582
  React.createElement(
541
583
  'div',
542
584
  { className: 'cb-section-card' },
@@ -547,6 +589,16 @@ window.__ModuleLoader__.load({
547
589
  React.createElement(
548
590
  'div',
549
591
  { className: 'cb-row' },
592
+ React.createElement(
593
+ 'button',
594
+ {
595
+ type: 'button',
596
+ className: 'cb-btn',
597
+ disabled: !!busy || !keyPresent,
598
+ onClick: handleSyncPlanModels,
599
+ },
600
+ busy === 'sync-models' ? 'Синхронизация…' : '🔄 Получить модели из плана'
601
+ ),
550
602
  React.createElement(
551
603
  'button',
552
604
  { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') },
@@ -572,7 +624,7 @@ window.__ModuleLoader__.load({
572
624
  React.createElement(
573
625
  'div',
574
626
  { className: 'cb-section-desc' },
575
- 'Отметьте галочками модели, которые должны отображаться в выпадающем списке выбора моделей в чате DeepSeek Harness.'
627
+ 'Список формируется напрямую из официальной подписки ClinePass без ручного ввода. Отметьте модели, которые должны отображаться в выпадающем списке выбора моделей в чате DeepSeek Harness.'
576
628
  ),
577
629
  React.createElement(
578
630
  'table',
@@ -612,9 +664,6 @@ window.__ModuleLoader__.load({
612
664
  React.createElement('strong', null, m.name),
613
665
  m.recommended
614
666
  ? 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
667
  : null
619
668
  ),
620
669
  React.createElement('td', null, React.createElement('code', null, m.id)),
@@ -623,7 +672,7 @@ window.__ModuleLoader__.load({
623
672
  'td',
624
673
  null,
625
674
  React.createElement('span', { className: 'cb-badge' }, m.category || 'general'),
626
- m.input?.includes('vision')
675
+ m.input?.includes('image') || m.input?.includes('vision')
627
676
  ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
628
677
  : null
629
678
  )
@@ -633,134 +682,48 @@ window.__ModuleLoader__.load({
633
682
  )
634
683
  ),
635
684
 
636
- // Card 4: Add New Custom Model Form
685
+ // Card 4: Session Metrics & Usage Tracking
637
686
  React.createElement(
638
687
  'div',
639
688
  { className: 'cb-section-card' },
640
689
  React.createElement(
641
690
  'div',
642
691
  { className: 'cb-section-title' },
643
- '➕ Добавить новую модель в подписку'
692
+ '📈 Статистика текущей сессии DSH'
644
693
  ),
645
694
  React.createElement(
646
695
  'div',
647
696
  { className: 'cb-section-desc' },
648
- 'Если в сервисе ClinePass появилась новая модель, добавьте её идентификатор сюда — она мгновенно станет доступна в DSH без обновления плагина.'
697
+ 'Локальный мониторинг запросов, задержек сети и ориентировочного расхода токенов через ClinePass.'
649
698
  ),
650
699
  React.createElement(
651
700
  'div',
652
701
  { className: 'cb-grid-2' },
653
702
  React.createElement(
654
703
  '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
- })
704
+ { className: 'cb-stat-box' },
705
+ React.createElement('div', { className: 'cb-stat-val' }, `${status.sessionStats?.successfulRequests || 0} / ${status.sessionStats?.totalRequests || 0}`),
706
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Успешных запросов / Всего')
663
707
  ),
664
708
  React.createElement(
665
709
  '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
- })
710
+ { className: 'cb-stat-box' },
711
+ React.createElement('div', { className: 'cb-stat-val' }, `~${status.sessionStats?.totalTokensEst || 0}`),
712
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Оценка токенов сессии')
674
713
  ),
675
714
  React.createElement(
676
715
  '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
- })
716
+ { className: 'cb-stat-box' },
717
+ React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs} мс` : '—'),
718
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Последняя задержка (Latency)')
685
719
  ),
686
720
  React.createElement(
687
721
  '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
- )
722
+ { className: 'cb-stat-box' },
723
+ React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastRequestAt ? new Date(status.sessionStats.lastRequestAt).toLocaleTimeString() : '—'),
724
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Время последнего запроса')
702
725
  )
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
726
+ )
764
727
  ),
765
728
 
766
729
  // Card 5: Diagnostics & Sync with DSH
@@ -826,8 +789,9 @@ window.__ModuleLoader__.load({
826
789
  }
827
790
 
828
791
  // Accordion item for Plugins section
829
- function PluginCard() {
792
+ function PluginCard(props) {
830
793
  const [open, setOpen] = React.useState(false)
794
+ const t = props?.t || makeT(ru, en)
831
795
  React.useEffect(() => {
832
796
  ensureCss()
833
797
  }, [])
@@ -855,41 +819,61 @@ window.__ModuleLoader__.load({
855
819
  React.createElement(
856
820
  'div',
857
821
  { 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)
822
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
823
+ React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
860
824
  ),
861
825
  React.createElement('span', { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
862
826
  React.createElement(Chevron)
863
827
  )
864
828
  ),
865
- open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage)) : null
829
+ open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage, { ...props, ctx: props?.ctx })) : null
866
830
  )
867
831
  }
868
832
 
869
833
  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
- })
834
+ if (ctx.locale && ctx.locale.register) {
835
+ try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
836
+ }
837
+ const t = (ctx.locale && ctx.locale.bind) ? ctx.locale.bind(NS) : makeT(ru, en)
838
+
839
+ let placed = false
840
+ try {
841
+ placed = !!ctx.slots.inject('settings.plugin.item', () => {
842
+ return ctx.slots.register(
843
+ {
844
+ name: 'settings.plugin.item',
845
+ key: NS,
846
+ locale: NS,
847
+ inject: () => ({ ctx }),
848
+ },
849
+ PluginCard
850
+ )
851
+ })
852
+ } catch (_) {
853
+ placed = false
854
+ }
882
855
 
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
- })
856
+ // Fallback to settings.section if settings.plugin.item is not declared
857
+ if (!placed) {
858
+ try {
859
+ ctx.slots.inject('settings.section', () => {
860
+ return ctx.slots.register(
861
+ {
862
+ name: 'settings.section',
863
+ id: '@goodandready/dsh-clinebot',
864
+ order: 28,
865
+ locale: NS,
866
+ label: () => t('title'),
867
+ inject: () => ({ ctx }),
868
+ },
869
+ SettingsPage
870
+ )
871
+ })
872
+ } catch (_) {}
873
+ }
890
874
  }
891
875
 
892
- module.exports = { apply, inject: ['slots'] }
876
+ module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
893
877
  return module.exports
894
878
  },
895
879
  })