@goodandready/dsh-clinebot 0.3.1 → 0.3.3

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
@@ -14,14 +14,164 @@ window.__ModuleLoader__.load({
14
14
  title: 'ClineBot',
15
15
  subtitle: 'ClinePass subscription ($9.99/mo) with curated & custom models.',
16
16
  'settings.loading': 'Loading ClineBot settings…',
17
+ 'settings.retry': 'Retry',
17
18
  'settings.unavailable': 'ClineBot settings unavailable (host namespace is not ready).',
19
+ 'header.title': 'ClineBot (ClinePass) Provider',
20
+ 'header.sub': 'Connect models under ClinePass subscription ($9.99/mo). OpenAI API compatible, automated rolling limit tracking, and instant auto-registration in DSH.',
21
+ 'badge.online': 'Host online ({latency} ms)',
22
+ 'badge.offline': 'Host unreachable',
23
+ 'badge.key_ok': 'Key ✓ ({source})',
24
+ 'badge.key_missing': 'Key missing',
25
+ 'badge.registered': 'DSH Registered ({count} models)',
26
+ 'badge.not_registered': 'Not registered',
27
+ 'key.title': '🔑 Authorization & API Key',
28
+ 'key.desc': 'The key is saved directly to DSH Credentials storage (~/.dsh/.credentials.yaml) and never exposed in plaintext configuration files.',
29
+ 'key.placeholder_has': '••••••••••••••••••••••••',
30
+ 'key.placeholder_empty': 'Paste ClinePass key (cline_...)',
31
+ 'key.show': 'Show',
32
+ 'key.hide': 'Hide',
33
+ 'key.save': 'Save Key',
34
+ 'key.saving': 'Saving…',
35
+ 'key.login_fast': '🚀 Quick Web Login',
36
+ 'key.login_waiting': 'Waiting for authorization in browser…',
37
+ 'key.env_label': 'Credential environment name: ',
38
+ 'key.get_key': 'Get key in app.cline.bot console ↗',
39
+ 'key.saved_msg': 'Key saved to DSH Credentials. Validation: {status}',
40
+ 'key.empty_err': 'Please enter an API key before saving',
41
+ 'accounts.title': '👥 Multi-Account Failover Pool',
42
+ 'accounts.desc': 'Add extra accounts. When an account hits 429 rate limits, ClineBot switches to the next available account automatically.',
43
+ 'accounts.active_badge': 'Active',
44
+ 'accounts.pinned_badge': 'Pinned',
45
+ 'accounts.pin_btn': 'Pin Active',
46
+ 'accounts.add_btn': '+ Add Account',
47
+ 'accounts.label_placeholder': 'Account Label (e.g. Work)',
48
+ 'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
49
+ 'quota.title': '📊 ClinePass Quota & Rate Limits',
50
+ 'quota.desc': 'Official rolling window request limits from ClinePass',
51
+ 'quota.account': 'Account: {email} · Plan: {plan}',
52
+ 'quota.refresh': 'Refresh Quotas',
53
+ 'quota.refreshing': 'Refreshing…',
54
+ 'quota.refreshed_msg': 'Subscription quotas refreshed',
55
+ 'quota.window_5h': '⏱ 5-Hour Rolling Limit',
56
+ 'quota.window_weekly': '📅 Weekly Window',
57
+ 'quota.used': '{pct}% used',
58
+ 'quota.remaining': 'Remaining: {pct}%',
59
+ 'quota.reset_at': 'Reset: {time}',
60
+ 'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
61
+ 'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker.',
62
+ 'models.sync': '🔄 Check Plan Models',
63
+ 'models.syncing': 'Checking…',
64
+ 'models.synced_msg': 'Synchronized subscription models: {total} ({discovered} discovered from ClinePass)',
65
+ 'models.all': 'All',
66
+ 'models.vision': 'Vision Only',
67
+ 'models.coding': 'Coding Only',
68
+ 'models.recommended': 'Recommended',
69
+ 'models.th_active': 'On',
70
+ 'models.th_name': 'Model Name',
71
+ 'models.th_id': 'Model ID',
72
+ 'models.th_ctx': 'Context',
73
+ 'models.th_caps': 'Capabilities',
74
+ 'models.star': 'Star',
75
+ 'models.auto_new': 'New',
76
+ 'stats.title': '📈 Current DSH Session Telemetry',
77
+ 'stats.desc': 'Local monitoring of requests, network latency, and estimated token usage via ClinePass.',
78
+ 'stats.requests': 'Successful / Total Requests',
79
+ 'stats.tokens': 'Estimated Session Tokens',
80
+ 'stats.latency': 'Last Request Latency',
81
+ 'stats.last_req': 'Last Request Time',
82
+ 'diag.title': '⚡ Diagnostics & Auto-Registration',
83
+ 'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test or force re-sync.',
84
+ 'diag.smoke_btn': 'Run Smoke Test (Ping)',
85
+ 'diag.smoke_testing': 'Testing…',
86
+ 'diag.smoke_ok': 'Smoke test passed! Latency: {latency} ms',
87
+ 'diag.resync_btn': 'Force Re-sync to DSH',
88
+ 'diag.resyncing': 'Syncing…',
89
+ 'diag.resynced_msg': 'Provider re-synced to DSH Models ({count} models)',
90
+ 'diag.unregister_btn': 'Unregister from DSH',
91
+ 'diag.unregistering': 'Removing…',
92
+ 'diag.unregistered_msg': 'Provider removed from DSH Models',
18
93
  }
19
94
 
20
95
  const ru = {
21
96
  title: 'ClineBot',
22
97
  subtitle: 'Подписка ClinePass ($9.99/мес) с каталогом проверенных моделей.',
23
98
  'settings.loading': 'Загрузка настроек ClineBot…',
99
+ 'settings.retry': 'Повторить попытку',
24
100
  'settings.unavailable': 'Настройки ClineBot недоступны (пространство хоста ещё не готово).',
101
+ 'header.title': 'Провайдер ClineBot (ClinePass)',
102
+ 'header.sub': 'Подключение моделей по подписке ClinePass ($9.99/мес). OpenAI API совместимый, автоматическое отслеживание лимитов и моментальная авторегистрация в DSH.',
103
+ 'badge.online': 'Сервер доступен ({latency} мс)',
104
+ 'badge.offline': 'Недоступен',
105
+ 'badge.key_ok': 'Ключ ✓ ({source})',
106
+ 'badge.key_missing': 'Ключ отсутствует',
107
+ 'badge.registered': 'Зарегистрирован в DSH ({count} мод.)',
108
+ 'badge.not_registered': 'Не зарегистрирован',
109
+ 'key.title': '🔑 Авторизация и API-ключ',
110
+ 'key.desc': 'Ключ сохраняется в системное хранилище DSH Credentials (~/.dsh/.credentials.yaml) и никогда не попадает в открытые файлы настроек.',
111
+ 'key.placeholder_has': '••••••••••••••••••••••••',
112
+ 'key.placeholder_empty': 'Вставьте ключ ClinePass (cline_...)',
113
+ 'key.show': 'Показать',
114
+ 'key.hide': 'Скрыть',
115
+ 'key.save': 'Сохранить ключ',
116
+ 'key.saving': 'Сохранение…',
117
+ 'key.login_fast': '🚀 Быстрый вход через браузер',
118
+ 'key.login_waiting': 'Ожидание авторизации в браузере…',
119
+ 'key.env_label': 'Переменная учётных данных: ',
120
+ 'key.get_key': 'Получить ключ в консоли app.cline.bot ↗',
121
+ 'key.saved_msg': 'Ключ сохранён в DSH Credentials. Валидация: {status}',
122
+ 'key.empty_err': 'Введите API-ключ перед сохранением',
123
+ 'accounts.title': '👥 Пул аккаунтов и авторотация (Failover)',
124
+ 'accounts.desc': 'Добавьте запасные аккаунты. При исчерпании лимитов (429) ClineBot автоматически переключит запрос на следующий доступный аккаунт.',
125
+ 'accounts.active_badge': 'Активен',
126
+ 'accounts.pinned_badge': 'Закреплен',
127
+ 'accounts.pin_btn': 'Сделать активным',
128
+ 'accounts.add_btn': '+ Добавить аккаунт',
129
+ 'accounts.label_placeholder': 'Название (например, Work)',
130
+ 'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
131
+ 'quota.title': '📊 Остаток лимитов подписки (ClinePass Quota)',
132
+ 'quota.desc': 'Официальные лимиты скользящих окон запросов ClinePass',
133
+ 'quota.account': 'Аккаунт: {email} · Тариф: {plan}',
134
+ 'quota.refresh': 'Обновить квоты',
135
+ 'quota.refreshing': 'Обновление…',
136
+ 'quota.refreshed_msg': 'Лимиты подписки обновлены',
137
+ 'quota.window_5h': '⏱ 5-часовое скользящее окно (Rolling Limit)',
138
+ 'quota.window_weekly': '📅 Недельное окно (Weekly Window)',
139
+ 'quota.used': '{pct}% использовано',
140
+ 'quota.remaining': 'Осталось: {pct}%',
141
+ 'quota.reset_at': 'Сброс: {time}',
142
+ 'models.title': '🎯 Модели в пикере DSH ({enabled} из {total} включено)',
143
+ 'models.desc': 'Список синхронизируется напрямую из подписки ClinePass. Новые модели появляются автоматически. Снимите галочку, чтобы скрыть ненужную модель из чата.',
144
+ 'models.sync': '🔄 Проверить модели плана',
145
+ 'models.syncing': 'Проверка…',
146
+ 'models.synced_msg': 'Синхронизировано моделей подписки: {total} ({discovered} получено напрямую из плана)',
147
+ 'models.all': 'Все',
148
+ 'models.vision': 'Только Vision',
149
+ 'models.coding': 'Только Кодинг',
150
+ 'models.recommended': 'Рекомендованные',
151
+ 'models.th_active': 'Вкл',
152
+ 'models.th_name': 'Название модели',
153
+ 'models.th_id': 'Model ID',
154
+ 'models.th_ctx': 'Контекст',
155
+ 'models.th_caps': 'Возможности',
156
+ 'models.star': 'Star',
157
+ 'models.auto_new': 'Новая',
158
+ 'stats.title': '📈 Статистика текущей сессии DSH',
159
+ 'stats.desc': 'Локальный мониторинг запросов, задержек сети и ориентировочного расхода токенов через ClinePass.',
160
+ 'stats.requests': 'Успешных запросов / Всего',
161
+ 'stats.tokens': 'Оценка токенов сессии',
162
+ 'stats.latency': 'Последняя задержка (Latency)',
163
+ 'stats.last_req': 'Время последнего запроса',
164
+ 'diag.title': '⚡ Диагностика и авторегистрация в DSH',
165
+ 'diag.desc': 'ClineBot автоматически регистрируется в DSH Models при наличии ключа. Вы можете запустить проверку связи или принудительно пересинхронизировать модели.',
166
+ 'diag.smoke_btn': 'Запустить Smoke Test (Ping)',
167
+ 'diag.smoke_testing': 'Тестирование…',
168
+ 'diag.smoke_ok': 'Smoke тест пройден! Задержка: {latency} мс',
169
+ 'diag.resync_btn': 'Принудительно обновить в DSH',
170
+ 'diag.resyncing': 'Обновление…',
171
+ 'diag.resynced_msg': 'Провайдер обновлен в DSH Models ({count} моделей)',
172
+ 'diag.unregister_btn': 'Удалить из DSH Models',
173
+ 'diag.unregistering': 'Удаление…',
174
+ 'diag.unregistered_msg': 'Провайдер удален из DSH Models',
25
175
  }
26
176
 
27
177
  function makeT(dict, fallback) {
@@ -85,7 +235,7 @@ window.__ModuleLoader__.load({
85
235
  .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}
86
236
  .cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
87
237
  .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}
238
+ .cb-btn-disabled{opacity:0.5;cursor:not-allowed}
89
239
 
90
240
  .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)}
91
241
  .cb-bar-head{display:flex;justify-content:space-between;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
@@ -111,7 +261,7 @@ window.__ModuleLoader__.load({
111
261
  document.head.appendChild(style)
112
262
  }
113
263
 
114
- function ProgressBar({ label, percentUsed, remainingPercent, resetsAt }) {
264
+ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
115
265
  const used = Math.max(0, Math.min(100, percentUsed || 0))
116
266
  let fillColor = 'var(--dsw-alias-state-success-primary)'
117
267
  if (used > 75) fillColor = 'var(--dsw-alias-state-warning-primary)'
@@ -126,7 +276,7 @@ window.__ModuleLoader__.load({
126
276
  'div',
127
277
  { className: 'cb-bar-head' },
128
278
  React.createElement('span', null, label),
129
- React.createElement('span', { style: { fontWeight: 600 } }, `${used}% использовано`)
279
+ React.createElement('span', { style: { fontWeight: 600 } }, t('quota.used', { pct: used }))
130
280
  ),
131
281
  React.createElement(
132
282
  'div',
@@ -139,8 +289,8 @@ window.__ModuleLoader__.load({
139
289
  React.createElement(
140
290
  'div',
141
291
  { className: 'cb-bar-meta' },
142
- React.createElement('span', null, `Осталось: ${remainingPercent ?? (100 - used)}%`),
143
- React.createElement('span', null, `Сброс: ${resetStr}`)
292
+ React.createElement('span', null, t('quota.remaining', { pct: remainingPercent ?? (100 - used) })),
293
+ React.createElement('span', null, t('quota.reset_at', { time: resetStr }))
144
294
  )
145
295
  )
146
296
  }
@@ -192,7 +342,7 @@ window.__ModuleLoader__.load({
192
342
  // Save Key handler
193
343
  async function handleSaveKey() {
194
344
  if (!apiKeyInput.trim()) {
195
- setErr('Введите API-ключ перед сохранением')
345
+ setErr(t('key.empty_err'))
196
346
  return
197
347
  }
198
348
  setBusy('save-key')
@@ -209,7 +359,7 @@ window.__ModuleLoader__.load({
209
359
  })
210
360
  const data = await res.json().catch(() => ({}))
211
361
  if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
212
- setMsg(`Ключ сохранён в DSH Credentials (~/.dsh/.credentials.yaml). Валидация: ${data.validated ? 'Успешно' : 'Ошибка'}`)
362
+ setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : 'Error' }))
213
363
  setApiKeyInput('')
214
364
  await load()
215
365
  } catch (e) {
@@ -229,7 +379,7 @@ window.__ModuleLoader__.load({
229
379
  const data = await res.json().catch(() => ({}))
230
380
  if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
231
381
  setStatus((prev) => ({ ...prev, usage: data }))
232
- setMsg('Лимиты подписки обновлены')
382
+ setMsg(t('quota.refreshed_msg'))
233
383
  } catch (e) {
234
384
  setErr(String(e.message || e))
235
385
  } finally {
@@ -246,7 +396,7 @@ window.__ModuleLoader__.load({
246
396
  const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
247
397
  const data = await res.json().catch(() => ({}))
248
398
  if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
249
- setMsg(`Синхронизировано моделей подписки: ${data.totalModelsCount} (${data.discoveredCount} получено напрямую из плана ClinePass)`)
399
+ setMsg(t('models.synced_msg', { total: data.totalModelsCount, discovered: data.discoveredCount }))
250
400
  await load()
251
401
  } catch (e) {
252
402
  setErr(String(e.message || e))
@@ -255,7 +405,7 @@ window.__ModuleLoader__.load({
255
405
  }
256
406
  }
257
407
 
258
- // Register/Sync Provider in DSH
408
+ // Register/Force Re-sync Provider in DSH
259
409
  async function handleRegister() {
260
410
  setBusy('register')
261
411
  setErr('')
@@ -268,7 +418,7 @@ window.__ModuleLoader__.load({
268
418
  })
269
419
  const data = await res.json().catch(() => ({}))
270
420
  if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
271
- setMsg(`Провайдер успешно зарегистрирован в DSH Models (${draft.enabledModels?.length || 0} моделей)`)
421
+ setMsg(t('diag.resynced_msg', { count: draft.enabledModels?.length || 0 }))
272
422
  await load()
273
423
  } catch (e) {
274
424
  setErr(String(e.message || e))
@@ -286,7 +436,7 @@ window.__ModuleLoader__.load({
286
436
  const res = await fetch(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
287
437
  const data = await res.json().catch(() => ({}))
288
438
  if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
289
- setMsg('Провайдер удален из DSH Models')
439
+ setMsg(t('diag.unregistered_msg'))
290
440
  await load()
291
441
  } catch (e) {
292
442
  setErr(String(e.message || e))
@@ -295,22 +445,37 @@ window.__ModuleLoader__.load({
295
445
  }
296
446
  }
297
447
 
298
- // Smoke chat test
299
- async function handleSmoke() {
300
- setBusy('smoke')
448
+ // Fast Browser Login
449
+ async function handleFastLogin() {
450
+ setBusy('fast-login')
301
451
  setErr('')
302
- setMsg('')
303
- setSmokeResult(null)
452
+ setMsg(t('key.login_waiting'))
304
453
  try {
305
- const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
454
+ const res = await fetch(`${ROUTE_PREFIX}/auth/begin`, { method: 'POST' })
455
+ const data = await res.json().catch(() => ({}))
456
+ if (!data.ok) throw new Error(data.error || 'Failed to initiate login')
457
+ if (data.authUrl) {
458
+ window.open(data.authUrl, '_blank')
459
+ }
460
+ } catch (e) {
461
+ setErr(String(e.message || e))
462
+ } finally {
463
+ setBusy('')
464
+ }
465
+ }
466
+
467
+ // Pin active account
468
+ async function handlePinAccount(accountEnv) {
469
+ setBusy(`pin-${accountEnv}`)
470
+ setErr('')
471
+ try {
472
+ const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
306
473
  method: 'POST',
307
474
  headers: { 'Content-Type': 'application/json' },
308
- body: JSON.stringify({ model: draft.defaultModel }),
475
+ body: JSON.stringify({ account: accountEnv }),
309
476
  })
310
477
  const data = await res.json().catch(() => ({}))
311
- if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
312
- setSmokeResult(data)
313
- setMsg(`Smoke тест пройден! Задержка: ${data.latencyMs} мс`)
478
+ if (!data.ok) throw new Error(data.error || 'Failed to switch account')
314
479
  await load()
315
480
  } catch (e) {
316
481
  setErr(String(e.message || e))
@@ -319,26 +484,31 @@ window.__ModuleLoader__.load({
319
484
  }
320
485
  }
321
486
 
322
- // Toggle model inclusion
487
+ // Toggle model exclusion (disabledModels logic)
323
488
  async function handleToggleModel(id) {
324
489
  if (!draft) return
325
- const current = new Set(draft.enabledModels || [])
326
- if (current.has(id)) {
327
- current.delete(id)
490
+ const currentDisabled = new Set(draft.disabledModels || [])
491
+ if (currentDisabled.has(id)) {
492
+ currentDisabled.delete(id) // Re-enable
328
493
  } else {
329
- current.add(id)
494
+ currentDisabled.add(id) // Disable
330
495
  }
331
- const nextList = Array.from(current)
332
- setDraft({ ...draft, enabledModels: nextList })
496
+ const nextDisabled = Array.from(currentDisabled)
497
+
498
+ // Compute next enabled models
499
+ const allIds = (status?.availableModels || []).map((m) => m.id)
500
+ const nextEnabled = allIds.filter((mId) => !currentDisabled.has(mId))
501
+
502
+ setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
333
503
 
334
504
  if (scope && snapshotStatus === 'ready') {
335
- try { await scope.set('enabledModels', nextList) } catch (_) {}
505
+ try { await scope.set('disabledModels', nextDisabled) } catch (_) {}
336
506
  }
337
507
  try {
338
508
  await fetch(`${ROUTE_PREFIX}/models/toggle`, {
339
509
  method: 'POST',
340
510
  headers: { 'Content-Type': 'application/json' },
341
- body: JSON.stringify({ enabledModels: nextList }),
511
+ body: JSON.stringify({ disabledModels: nextDisabled }),
342
512
  })
343
513
  } catch {}
344
514
  }
@@ -346,26 +516,30 @@ window.__ModuleLoader__.load({
346
516
  // Select all / filter models
347
517
  async function handleSetModelsFilter(type) {
348
518
  if (!status?.availableModels) return
349
- let nextList = []
519
+ const all = status.availableModels
520
+ let allowed = new Set()
350
521
  if (type === 'all') {
351
- nextList = status.availableModels.map((m) => m.id)
522
+ allowed = new Set(all.map((m) => m.id))
352
523
  } else if (type === 'vision') {
353
- nextList = status.availableModels.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id)
524
+ allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
354
525
  } else if (type === 'coding') {
355
- nextList = status.availableModels.filter((m) => m.category === 'coding').map((m) => m.id)
526
+ allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
356
527
  } else if (type === 'recommended') {
357
- nextList = status.availableModels.filter((m) => m.recommended).map((m) => m.id)
528
+ allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
358
529
  }
359
530
 
360
- setDraft({ ...draft, enabledModels: nextList })
531
+ const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
532
+ const nextEnabled = Array.from(allowed)
533
+
534
+ setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
361
535
  if (scope && snapshotStatus === 'ready') {
362
- try { await scope.set('enabledModels', nextList) } catch (_) {}
536
+ try { await scope.set('disabledModels', nextDisabled) } catch (_) {}
363
537
  }
364
538
  try {
365
539
  await fetch(`${ROUTE_PREFIX}/models/toggle`, {
366
540
  method: 'POST',
367
541
  headers: { 'Content-Type': 'application/json' },
368
- body: JSON.stringify({ enabledModels: nextList }),
542
+ body: JSON.stringify({ disabledModels: nextDisabled }),
369
543
  })
370
544
  } catch {}
371
545
  }
@@ -383,30 +557,31 @@ window.__ModuleLoader__.load({
383
557
  return React.createElement(
384
558
  'div',
385
559
  { className: 'cb-page' },
386
- React.createElement('div', { className: 'cb-alert cb-alert-err' }, `Ошибка загрузки настроек ClineBot: ${err}`),
560
+ React.createElement('div', { className: 'cb-alert cb-alert-err' }, `${t('settings.loading')}: ${err}`),
387
561
  React.createElement(
388
562
  'button',
389
563
  {
390
564
  type: 'button',
391
- className: 'cb-btn cb-btn-secondary',
565
+ className: 'cb-btn',
392
566
  style: { marginTop: '12px', alignSelf: 'flex-start' },
393
567
  onClick: () => {
394
568
  setErr('')
395
569
  load().catch((e) => setErr(String(e.message || e)))
396
570
  },
397
571
  },
398
- 'Повторить попытку'
572
+ t('settings.retry')
399
573
  )
400
574
  )
401
575
  }
402
- return React.createElement('div', { className: 'cb-page' }, 'Загрузка настроек ClineBot…')
576
+ return React.createElement('div', { className: 'cb-page' }, t('settings.loading'))
403
577
  }
404
578
 
405
579
  const healthOk = !!status.health?.ok
406
580
  const keyPresent = !!status.key?.present
407
581
  const isRegistered = !!status.isRegistered
408
582
  const modelsList = status.availableModels || []
409
- const enabledSet = new Set(draft.enabledModels || [])
583
+ const disabledSet = new Set(draft.disabledModels || [])
584
+ const enabledCount = modelsList.filter((m) => !disabledSet.has(m.id)).length
410
585
  const usage = status.usage
411
586
 
412
587
  return React.createElement(
@@ -420,27 +595,27 @@ window.__ModuleLoader__.load({
420
595
  React.createElement(
421
596
  'div',
422
597
  { className: 'cb-page-title' },
423
- '🤖 ClineBot (ClinePass) Provider',
598
+ `🤖 ${t('header.title')}`,
424
599
  React.createElement(
425
600
  'span',
426
601
  { className: `cb-badge ${healthOk ? 'cb-badge-ok' : 'cb-badge-bad'}` },
427
- healthOk ? `Сервер доступен (${status.health?.latencyMs} мс)` : 'Недоступен'
602
+ healthOk ? t('badge.online', { latency: status.health?.latencyMs }) : t('badge.offline')
428
603
  ),
429
604
  React.createElement(
430
605
  'span',
431
606
  { className: `cb-badge ${keyPresent ? 'cb-badge-ok' : 'cb-badge-warn'}` },
432
- keyPresent ? `Ключ ✓ (${status.key?.source})` : 'Ключ отсутствует'
607
+ keyPresent ? t('badge.key_ok', { source: status.key?.source }) : t('badge.key_missing')
433
608
  ),
434
609
  React.createElement(
435
610
  'span',
436
611
  { className: `cb-badge ${isRegistered ? 'cb-badge-ok' : 'cb-badge-warn'}` },
437
- isRegistered ? 'Зарегистрирован в DSH' : 'Не зарегистрирован'
612
+ isRegistered ? t('badge.registered', { count: enabledCount }) : t('badge.not_registered')
438
613
  )
439
614
  ),
440
615
  React.createElement(
441
616
  'div',
442
617
  { className: 'cb-page-sub' },
443
- 'Подключение моделей по подписке ClinePass ($9.99/мес). Провайдер совместим с OpenAI API, автоматически отслеживает скользящие лимиты и регистрирует проверенный каталог моделей в DSH.'
618
+ t('header.sub')
444
619
  )
445
620
  ),
446
621
 
@@ -455,12 +630,12 @@ window.__ModuleLoader__.load({
455
630
  React.createElement(
456
631
  'div',
457
632
  { className: 'cb-section-title' },
458
- '🔑 Авторизация и API-ключ'
633
+ t('key.title')
459
634
  ),
460
635
  React.createElement(
461
636
  'div',
462
637
  { className: 'cb-section-desc' },
463
- 'Ключ сохраняется в системное хранилище DSH Credentials (~/.dsh/.credentials.yaml) и никогда не попадает в открытые файлы настроек.'
638
+ t('key.desc')
464
639
  ),
465
640
  React.createElement(
466
641
  'div',
@@ -468,7 +643,7 @@ window.__ModuleLoader__.load({
468
643
  React.createElement('input', {
469
644
  className: 'cb-input',
470
645
  type: showKey ? 'text' : 'password',
471
- placeholder: keyPresent ? '••••••••••••••••••••••••' : 'Вставьте ключ ClinePass (cline_...)',
646
+ placeholder: keyPresent ? t('key.placeholder_has') : t('key.placeholder_empty'),
472
647
  value: apiKeyInput,
473
648
  onChange: (e) => setApiKeyInput(e.target.value),
474
649
  }),
@@ -479,7 +654,7 @@ window.__ModuleLoader__.load({
479
654
  className: 'cb-btn',
480
655
  onClick: () => setShowKey((v) => !v),
481
656
  },
482
- showKey ? 'Скрыть' : 'Показать'
657
+ showKey ? t('key.hide') : t('key.show')
483
658
  ),
484
659
  React.createElement(
485
660
  'button',
@@ -489,13 +664,23 @@ window.__ModuleLoader__.load({
489
664
  disabled: !!busy || !apiKeyInput.trim(),
490
665
  onClick: handleSaveKey,
491
666
  },
492
- busy === 'save-key' ? 'Сохранение…' : 'Сохранить ключ'
667
+ busy === 'save-key' ? t('key.saving') : t('key.save')
668
+ ),
669
+ React.createElement(
670
+ 'button',
671
+ {
672
+ type: 'button',
673
+ className: 'cb-btn',
674
+ disabled: !!busy,
675
+ onClick: handleFastLogin,
676
+ },
677
+ busy === 'fast-login' ? '…' : t('key.login_fast')
493
678
  )
494
679
  ),
495
680
  React.createElement(
496
681
  'div',
497
682
  { className: 'cb-row', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
498
- React.createElement('span', null, `Переменная учётных данных: `),
683
+ React.createElement('span', null, t('key.env_label')),
499
684
  React.createElement('code', null, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
500
685
  React.createElement('span', null, ` · `),
501
686
  React.createElement(
@@ -506,11 +691,85 @@ window.__ModuleLoader__.load({
506
691
  rel: 'noreferrer',
507
692
  style: { color: 'var(--dsw-alias-state-brand-primary)' },
508
693
  },
509
- 'Получить ключ в консоли app.cline.bot ↗'
694
+ t('key.get_key')
510
695
  )
511
696
  )
512
697
  ),
513
698
 
699
+ // Accounts Pool Card
700
+ status.accounts && status.accounts.length > 0
701
+ ? React.createElement(
702
+ 'div',
703
+ { className: 'cb-section-card' },
704
+ React.createElement(
705
+ 'div',
706
+ { className: 'cb-section-title' },
707
+ t('accounts.title')
708
+ ),
709
+ React.createElement(
710
+ 'div',
711
+ { className: 'cb-section-desc' },
712
+ t('accounts.desc')
713
+ ),
714
+ React.createElement(
715
+ 'table',
716
+ { className: 'cb-table' },
717
+ React.createElement(
718
+ 'thead',
719
+ null,
720
+ React.createElement(
721
+ 'tr',
722
+ null,
723
+ React.createElement('th', null, 'Account'),
724
+ React.createElement('th', null, 'Credential Ref'),
725
+ React.createElement('th', null, 'Status'),
726
+ React.createElement('th', { style: { textAlign: 'right' } }, 'Action')
727
+ )
728
+ ),
729
+ React.createElement(
730
+ 'tbody',
731
+ null,
732
+ status.accounts.map((acc) => {
733
+ const isActive = status.activeAccount === acc.apiKeyEnv || (!status.activeAccount && acc.id === 'default')
734
+ return React.createElement(
735
+ 'tr',
736
+ { key: acc.id },
737
+ React.createElement('td', null, React.createElement('strong', null, acc.label)),
738
+ React.createElement('td', null, React.createElement('code', null, acc.apiKeyEnv)),
739
+ React.createElement(
740
+ 'td',
741
+ null,
742
+ acc.present
743
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, `Configured (${acc.source})`)
744
+ : React.createElement('span', { className: 'cb-badge cb-badge-warn' }, 'Missing Key'),
745
+ isActive
746
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('accounts.active_badge'))
747
+ : null
748
+ ),
749
+ React.createElement(
750
+ 'td',
751
+ { style: { textAlign: 'right' } },
752
+ !isActive && acc.present
753
+ ? React.createElement(
754
+ 'button',
755
+ {
756
+ type: 'button',
757
+ className: 'cb-btn',
758
+ style: { padding: '4px 8px', fontSize: '11px' },
759
+ disabled: !!busy,
760
+ onClick: () => handlePinAccount(acc.apiKeyEnv),
761
+ },
762
+ t('accounts.pin_btn')
763
+ )
764
+ : null
765
+ )
766
+ )
767
+ })
768
+ )
769
+ )
770
+ )
771
+ : null,
772
+
514
773
  // Quota Warning Banner
515
774
  status.quotaWarning
516
775
  ? React.createElement(
@@ -525,7 +784,7 @@ window.__ModuleLoader__.load({
525
784
  ? React.createElement(
526
785
  'div',
527
786
  { style: { fontSize: '11px', opacity: 0.9, marginTop: '2px' } },
528
- `Ожидаемое время сброса лимита: ${new Date(status.quotaWarning.resetsAt).toLocaleTimeString()}`
787
+ t('quota.reset_at', { time: new Date(status.quotaWarning.resetsAt).toLocaleTimeString() })
529
788
  )
530
789
  : null
531
790
  )
@@ -540,7 +799,7 @@ window.__ModuleLoader__.load({
540
799
  React.createElement(
541
800
  'div',
542
801
  { className: 'cb-section-title' },
543
- '📊 Остаток лимитов подписки (ClinePass Quota)',
802
+ t('quota.title'),
544
803
  React.createElement(
545
804
  'button',
546
805
  {
@@ -549,43 +808,45 @@ window.__ModuleLoader__.load({
549
808
  disabled: !!busy,
550
809
  onClick: handleRefreshQuota,
551
810
  },
552
- busy === 'refresh-quota' ? 'Обновление…' : 'Обновить квоты'
811
+ busy === 'refresh-quota' ? t('quota.refreshing') : t('quota.refresh')
553
812
  )
554
813
  ),
555
814
  React.createElement(
556
815
  'div',
557
816
  { className: 'cb-section-desc' },
558
817
  usage?.user?.email
559
- ? `Аккаунт: ${usage.user.email} · Тариф: ${usage.plan || 'ClinePass ($9.99/mo)'}`
560
- : 'Официальные лимиты скользящих окон запросов ClinePass'
818
+ ? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass ($9.99/mo)' })
819
+ : t('quota.desc')
561
820
  ),
562
821
  React.createElement(
563
822
  'div',
564
823
  { className: 'cb-grid-2' },
565
824
  React.createElement(ProgressBar, {
566
- label: '⏱ 5-часовое скользящее окно (Rolling Limit)',
825
+ label: t('quota.window_5h'),
567
826
  percentUsed: usage?.windows?.fiveHour?.percentUsed || 0,
568
827
  remainingPercent: usage?.windows?.fiveHour?.remainingPercent || 100,
569
828
  resetsAt: usage?.windows?.fiveHour?.resetsAt,
829
+ t,
570
830
  }),
571
831
  React.createElement(ProgressBar, {
572
- label: '📅 Недельное окно (Weekly Window)',
832
+ label: t('quota.window_weekly'),
573
833
  percentUsed: usage?.windows?.weekly?.percentUsed || 0,
574
834
  remainingPercent: usage?.windows?.weekly?.remainingPercent || 100,
575
835
  resetsAt: usage?.windows?.weekly?.resetsAt,
836
+ t,
576
837
  })
577
838
  )
578
839
  )
579
840
  : null,
580
841
 
581
- // Card 3: Model Picker Management with Official Plan Sync
842
+ // Card 3: Model Picker Management with Official Plan Sync & Auto-Enable
582
843
  React.createElement(
583
844
  'div',
584
845
  { className: 'cb-section-card' },
585
846
  React.createElement(
586
847
  'div',
587
848
  { className: 'cb-section-title' },
588
- `🎯 Модели в пикере DSH (${enabledSet.size} из ${modelsList.length} включено)`,
849
+ t('models.title', { enabled: enabledCount, total: modelsList.length }),
589
850
  React.createElement(
590
851
  'div',
591
852
  { className: 'cb-row' },
@@ -597,34 +858,34 @@ window.__ModuleLoader__.load({
597
858
  disabled: !!busy || !keyPresent,
598
859
  onClick: handleSyncPlanModels,
599
860
  },
600
- busy === 'sync-models' ? 'Синхронизация…' : '🔄 Получить модели из плана'
861
+ busy === 'sync-models' ? t('models.syncing') : t('models.sync')
601
862
  ),
602
863
  React.createElement(
603
864
  'button',
604
865
  { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') },
605
- 'Все'
866
+ t('models.all')
606
867
  ),
607
868
  React.createElement(
608
869
  'button',
609
870
  { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('vision') },
610
- 'Только Vision'
871
+ t('models.vision')
611
872
  ),
612
873
  React.createElement(
613
874
  'button',
614
875
  { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('coding') },
615
- 'Только Кодинг'
876
+ t('models.coding')
616
877
  ),
617
878
  React.createElement(
618
879
  'button',
619
880
  { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('recommended') },
620
- 'Рекомендованные'
881
+ t('models.recommended')
621
882
  )
622
883
  )
623
884
  ),
624
885
  React.createElement(
625
886
  'div',
626
887
  { className: 'cb-section-desc' },
627
- 'Список формируется напрямую из официальной подписки ClinePass без ручного ввода. Отметьте модели, которые должны отображаться в выпадающем списке выбора моделей в чате DeepSeek Harness.'
888
+ t('models.desc')
628
889
  ),
629
890
  React.createElement(
630
891
  'table',
@@ -635,18 +896,19 @@ window.__ModuleLoader__.load({
635
896
  React.createElement(
636
897
  'tr',
637
898
  null,
638
- React.createElement('th', { style: { width: '40px' } }, 'Вкл'),
639
- React.createElement('th', null, 'Название модели'),
640
- React.createElement('th', null, 'Model ID'),
641
- React.createElement('th', null, 'Контекст'),
642
- React.createElement('th', null, 'Возможности')
899
+ React.createElement('th', { style: { width: '40px' } }, t('models.th_active')),
900
+ React.createElement('th', null, t('models.th_name')),
901
+ React.createElement('th', null, t('models.th_id')),
902
+ React.createElement('th', null, t('models.th_ctx')),
903
+ React.createElement('th', null, t('models.th_caps'))
643
904
  )
644
905
  ),
645
906
  React.createElement(
646
907
  'tbody',
647
908
  null,
648
- modelsList.map((m) =>
649
- React.createElement(
909
+ modelsList.map((m) => {
910
+ const isEnabled = !disabledSet.has(m.id)
911
+ return React.createElement(
650
912
  'tr',
651
913
  { key: m.id },
652
914
  React.createElement(
@@ -654,7 +916,7 @@ window.__ModuleLoader__.load({
654
916
  null,
655
917
  React.createElement('input', {
656
918
  type: 'checkbox',
657
- checked: enabledSet.has(m.id),
919
+ checked: isEnabled,
658
920
  onChange: () => handleToggleModel(m.id),
659
921
  })
660
922
  ),
@@ -663,7 +925,10 @@ window.__ModuleLoader__.load({
663
925
  null,
664
926
  React.createElement('strong', null, m.name),
665
927
  m.recommended
666
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, 'Star')
928
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('models.star'))
929
+ : null,
930
+ m.isCustom
931
+ ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '6px', opacity: 0.8 } }, t('models.auto_new'))
667
932
  : null
668
933
  ),
669
934
  React.createElement('td', null, React.createElement('code', null, m.id)),
@@ -674,10 +939,13 @@ window.__ModuleLoader__.load({
674
939
  React.createElement('span', { className: 'cb-badge' }, m.category || 'general'),
675
940
  m.input?.includes('image') || m.input?.includes('vision')
676
941
  ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
942
+ : null,
943
+ m.reasoningEfforts?.length || m.reasoning
944
+ ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, '🧠 Reasoning')
677
945
  : null
678
946
  )
679
947
  )
680
- )
948
+ })
681
949
  )
682
950
  )
683
951
  ),
@@ -689,12 +957,12 @@ window.__ModuleLoader__.load({
689
957
  React.createElement(
690
958
  'div',
691
959
  { className: 'cb-section-title' },
692
- '📈 Статистика текущей сессии DSH'
960
+ t('stats.title')
693
961
  ),
694
962
  React.createElement(
695
963
  'div',
696
964
  { className: 'cb-section-desc' },
697
- 'Локальный мониторинг запросов, задержек сети и ориентировочного расхода токенов через ClinePass.'
965
+ t('stats.desc')
698
966
  ),
699
967
  React.createElement(
700
968
  'div',
@@ -703,25 +971,25 @@ window.__ModuleLoader__.load({
703
971
  'div',
704
972
  { className: 'cb-stat-box' },
705
973
  React.createElement('div', { className: 'cb-stat-val' }, `${status.sessionStats?.successfulRequests || 0} / ${status.sessionStats?.totalRequests || 0}`),
706
- React.createElement('div', { className: 'cb-stat-lbl' }, 'Успешных запросов / Всего')
974
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.requests'))
707
975
  ),
708
976
  React.createElement(
709
977
  'div',
710
978
  { className: 'cb-stat-box' },
711
979
  React.createElement('div', { className: 'cb-stat-val' }, `~${status.sessionStats?.totalTokensEst || 0}`),
712
- React.createElement('div', { className: 'cb-stat-lbl' }, 'Оценка токенов сессии')
980
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.tokens'))
713
981
  ),
714
982
  React.createElement(
715
983
  'div',
716
984
  { 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)')
985
+ React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs} ms` : '—'),
986
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.latency'))
719
987
  ),
720
988
  React.createElement(
721
989
  'div',
722
990
  { className: 'cb-stat-box' },
723
991
  React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastRequestAt ? new Date(status.sessionStats.lastRequestAt).toLocaleTimeString() : '—'),
724
- React.createElement('div', { className: 'cb-stat-lbl' }, 'Время последнего запроса')
992
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.last_req'))
725
993
  )
726
994
  )
727
995
  ),
@@ -733,12 +1001,12 @@ window.__ModuleLoader__.load({
733
1001
  React.createElement(
734
1002
  'div',
735
1003
  { className: 'cb-section-title' },
736
- '⚡ Диагностика и регистрация в DSH Models'
1004
+ t('diag.title')
737
1005
  ),
738
1006
  React.createElement(
739
1007
  'div',
740
1008
  { className: 'cb-section-desc' },
741
- 'Проверьте связь с эндпоинтом и синхронизируйте активный список моделей с ядром DeepSeek Harness.'
1009
+ t('diag.desc')
742
1010
  ),
743
1011
  React.createElement(
744
1012
  'div',
@@ -751,7 +1019,7 @@ window.__ModuleLoader__.load({
751
1019
  disabled: !!busy || !keyPresent,
752
1020
  onClick: handleSmoke,
753
1021
  },
754
- busy === 'smoke' ? 'Тестирование…' : 'Запустить Smoke Test (Ping)'
1022
+ busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
755
1023
  ),
756
1024
  isRegistered
757
1025
  ? React.createElement(
@@ -762,7 +1030,7 @@ window.__ModuleLoader__.load({
762
1030
  disabled: !!busy,
763
1031
  onClick: handleUnregister,
764
1032
  },
765
- busy === 'unregister' ? 'Удаление…' : 'Удалить из DSH Models'
1033
+ busy === 'unregister' ? t('diag.unregistering') : t('diag.unregister_btn')
766
1034
  )
767
1035
  : null,
768
1036
  React.createElement(
@@ -774,14 +1042,14 @@ window.__ModuleLoader__.load({
774
1042
  disabled: !!busy,
775
1043
  onClick: handleRegister,
776
1044
  },
777
- busy === 'register' ? 'Синхронизация…' : 'Синхронизировать с DSH Models'
1045
+ busy === 'register' ? t('diag.resyncing') : t('diag.resync_btn')
778
1046
  )
779
1047
  ),
780
1048
  smokeResult
781
1049
  ? React.createElement(
782
1050
  'div',
783
1051
  { className: 'cb-preview' },
784
- `✅ Задержка ответа: ${smokeResult.latencyMs} мс | Модель: ${smokeResult.model}\nОтвет: ${smokeResult.preview || '(пустой ответ)'}`
1052
+ `✅ Latency: ${smokeResult.latencyMs} ms | Model: ${smokeResult.model}\nResponse: ${smokeResult.preview || '(empty)'}`
785
1053
  )
786
1054
  : null
787
1055
  )