@goodandready/dsh-clinebot 0.3.10 → 0.3.12

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
@@ -1,14 +1,19 @@
1
- // Plugin Settings Card (settings.plugin.item).
1
+ // @generated by scripts/build-client.js - DO NOT EDIT DIRECTLY.
2
+ // Edit modular source files in src/client/ instead.
3
+
2
4
  window.__ModuleLoader__.load({
3
5
  id: '@goodandready/dsh-clinebot',
4
6
  factory: (require) => {
5
7
  var module = { exports: {} }
6
- const React = require('react')
7
- const NS = 'dsh-clinebot'
8
- const TITLE = 'ClineBot'
9
- const SUBTITLE = 'ClinePass subscription ($9.99/mo) with curated & custom models.'
10
- const ROUTE_PREFIX = '/dsh-clinebot'
8
+ var exports = module.exports
9
+ let React = require('react')
10
+
11
+ // --- src/client/constants.js ---
12
+ const NS = 'dsh-clinebot'
13
+ const ROUTE_PREFIX = '/dsh-clinebot'
14
+ const SNAPSHOT_LOADING = { status: 'loading', view: null }
11
15
 
16
+ // --- src/client/locales.js ---
12
17
  const en = {
13
18
  title: 'ClineBot',
14
19
  subtitle: 'ClinePass subscription ($9.99/mo) with curated & custom models.',
@@ -94,6 +99,8 @@ const en = {
94
99
  'update.updating': 'Updating…',
95
100
  'update.done': 'Successfully updated to v{version}! Please restart DSH.',
96
101
  'update.failed': 'Update failed: {error}',
102
+ 'update.checking': 'Checking for updates…',
103
+ 'update.up_to_date': 'Up to date',
97
104
  }
98
105
 
99
106
  const zh = {
@@ -181,6 +188,8 @@ const en = {
181
188
  'update.updating': '正在更新…',
182
189
  'update.done': '更新成功至 v{version}!请重启 DSH 服务生效。',
183
190
  'update.failed': '更新失败:{error}',
191
+ 'update.checking': '正在检查更新…',
192
+ 'update.up_to_date': '已是最新版本',
184
193
  }
185
194
 
186
195
  function makeT(dict, fallback) {
@@ -195,29 +204,31 @@ const en = {
195
204
  }
196
205
  }
197
206
 
198
- function FallbackChevron() {
199
- return React.createElement(
200
- 'svg',
201
- { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
202
- React.createElement('path', {
203
- d: 'M3.5 5.25L7 8.75L10.5 5.25',
204
- stroke: 'currentColor',
205
- strokeWidth: 1.5,
206
- strokeLinecap: 'round',
207
- strokeLinejoin: 'round',
208
- })
209
- )
210
- }
211
- let ChevronIcon = null
212
- try {
213
- const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
214
- ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
215
- } catch (_) {
216
- ChevronIcon = null
217
- }
218
- const Chevron = ChevronIcon || FallbackChevron
219
-
220
- function ensureCss() {
207
+ // --- src/client/chevron.js ---
208
+ function FallbackChevron() {
209
+ return React.createElement(
210
+ 'svg',
211
+ { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
212
+ React.createElement('path', {
213
+ d: 'M3.5 5.25L7 8.75L10.5 5.25',
214
+ stroke: 'currentColor',
215
+ strokeWidth: 1.5,
216
+ strokeLinecap: 'round',
217
+ strokeLinejoin: 'round',
218
+ })
219
+ )
220
+ }
221
+ let ChevronIcon = null
222
+ try {
223
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
224
+ ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
225
+ } catch (_) {
226
+ ChevronIcon = null
227
+ }
228
+ const Chevron = ChevronIcon || FallbackChevron
229
+
230
+ // --- src/client/styles.js ---
231
+ function ensureCss() {
221
232
  if (typeof document === 'undefined') return
222
233
  if (document.getElementById('dsh-clinebot-full-css')) return
223
234
  const style = document.createElement('style')
@@ -279,1079 +290,1162 @@ const en = {
279
290
  document.head.appendChild(style)
280
291
  }
281
292
 
282
- function createErrorBoundary() {
283
- if (!React || typeof React.Component !== 'function') {
284
- return function NoopBoundary(props) { return props?.children || null }
285
- }
286
- return class ErrorBoundary extends React.Component {
287
- constructor(props) {
288
- super(props)
289
- this.state = { hasError: false, error: null }
290
- }
291
- static getDerivedStateFromError(error) {
292
- return { hasError: true, error }
293
- }
294
- componentDidCatch(error, errorInfo) {
295
- console.error('[dsh-clinebot] React Error:', error, errorInfo)
296
- }
297
- render() {
298
- if (this.state.hasError) {
299
- return React.createElement(
300
- 'div',
301
- {
302
- className: 'cb-alert cb-alert-err',
303
- style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
304
- },
305
- React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ClineBot UI Error:'),
306
- React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
307
- React.createElement(
308
- 'button',
309
- {
310
- type: 'button',
311
- className: 'cb-btn',
312
- style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
313
- onClick: () => this.setState({ hasError: false, error: null }),
314
- },
315
- 'Retry'
316
- )
317
- )
318
- }
319
- return this.props?.children || null
320
- }
321
- }
322
- }
323
- const ErrorBoundary = createErrorBoundary()
324
-
325
- const SNAPSHOT_READY = Object.freeze({ status: 'ready', value: {} })
326
- const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', value: {} })
327
-
328
- function formatResetTime(isoString) {
329
- if (!isoString) return '—'
330
- try {
331
- const d = new Date(isoString)
332
- if (isNaN(d.getTime())) return '—'
333
- return d.toLocaleTimeString()
334
- } catch {
335
- return '—'
336
- }
337
- }
338
-
339
- function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
340
- const used = Math.max(0, Math.min(100, percentUsed || 0))
341
- let fillColor = 'var(--dsw-alias-state-success-primary)'
342
- if (used > 75) fillColor = 'var(--dsw-alias-state-warning-primary)'
343
- if (used > 90) fillColor = 'var(--dsw-alias-state-error-primary)'
344
-
345
- const resetStr = formatResetTime(resetsAt)
346
-
347
- return React.createElement(
348
- 'div',
349
- { className: 'cb-bar-container' },
350
- React.createElement(
351
- 'div',
352
- { className: 'cb-bar-head' },
353
- React.createElement('span', null, label),
354
- React.createElement('span', { style: { fontWeight: 600 } }, t('quota.used', { pct: used }))
355
- ),
356
- React.createElement(
357
- 'div',
358
- { className: 'cb-bar-track' },
359
- React.createElement('div', {
360
- className: 'cb-bar-fill',
361
- style: { width: `${used}%`, background: fillColor },
362
- })
363
- ),
364
- React.createElement(
365
- 'div',
366
- { className: 'cb-bar-meta' },
367
- React.createElement('span', null, t('quota.remaining', { pct: remainingPercent ?? (100 - used) })),
368
- React.createElement('span', null, t('quota.reset_at', { time: resetStr }))
369
- )
370
- )
293
+ // --- src/client/error-boundary.js ---
294
+ function createErrorBoundary() {
295
+ try {
296
+ if (!React || typeof React.Component !== 'function') {
297
+ return function NoopBoundary(props) { return props?.children || null }
371
298
  }
372
-
373
- function SettingsPage(props) {
374
- const ctx = props?.ctx
375
- const t = props?.t || makeT(en, en)
376
-
377
- const scope = React.useMemo(() => {
378
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
379
- if (!s?.bind) return undefined
380
- try {
381
- return s.bind({ namespace: NS })
382
- } catch (_) {
383
- return undefined
384
- }
385
- }, [ctx])
386
-
387
- const subscribe = React.useMemo(() => {
388
- return (cb) => {
389
- if (!scope?.subscribe) return () => {}
390
- try {
391
- return scope.subscribe(cb) || (() => {})
392
- } catch (_) {
393
- return () => {}
394
- }
395
- }
396
- }, [scope])
397
-
398
- const getSnapshot = React.useCallback(() => {
399
- if (!scope?.getSnapshot) return SNAPSHOT_LOADING
400
- try {
401
- return scope.getSnapshot() || SNAPSHOT_LOADING
402
- } catch (_) {
403
- return SNAPSHOT_LOADING
404
- }
405
- }, [scope])
406
-
407
- const snapshot = React.useSyncExternalStore(
408
- subscribe,
409
- getSnapshot,
410
- React.useCallback(() => SNAPSHOT_LOADING, [])
411
- )
412
- const snapshotStatus = snapshot?.status || 'loading'
413
-
414
- const [status, setStatus] = React.useState(null)
415
- const [draft, setDraft] = React.useState(null)
416
- const [busy, setBusy] = React.useState('')
417
- const [err, setErr] = React.useState('')
418
- const [msg, setMsg] = React.useState('')
419
- const [smokeResult, setSmokeResult] = React.useState(null)
420
-
421
- // Key input state
422
- const [apiKeyInput, setApiKeyInput] = React.useState('')
423
- const [showKey, setShowKey] = React.useState(false)
424
-
425
- React.useEffect(() => {
426
- ensureCss()
427
- }, [])
428
-
429
- const load = React.useCallback(async () => {
430
- setErr('')
431
- const res = await fetch(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
432
- const data = await res.json().catch(() => ({}))
433
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
434
- setStatus(data)
435
- setDraft(data.config || {})
436
- }, [])
437
-
438
- React.useEffect(() => {
439
- load().catch((e) => setErr(String(e.message || e)))
440
- }, [load])
441
-
442
- React.useEffect(() => {
443
- if (snapshotStatus === 'ready' && snapshot?.value && typeof snapshot.value === 'object') {
444
- setDraft((prev) => (prev ? { ...prev, ...snapshot.value } : snapshot.value))
445
- }
446
- }, [snapshotStatus, snapshot?.value])
447
-
448
- // Save Key handler
449
- async function handleSaveKey() {
450
- if (!apiKeyInput.trim()) {
451
- setErr(t('key.empty_err'))
452
- return
453
- }
454
- setBusy('save-key')
455
- setErr('')
456
- setMsg('')
457
- try {
458
- const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
459
- method: 'POST',
460
- headers: { 'Content-Type': 'application/json' },
461
- body: JSON.stringify({
462
- apiKey: apiKeyInput.trim(),
463
- apiKeyEnv: draft?.apiKeyEnv || 'CLINEBOT_API_KEY',
464
- }),
465
- })
466
- const data = await res.json().catch(() => ({}))
467
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
468
- setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : 'Error' }))
469
- setApiKeyInput('')
470
- await load()
471
- } catch (e) {
472
- setErr(String(e.message || e))
473
- } finally {
474
- setBusy('')
475
- }
476
- }
477
-
478
- // Refresh Quota handler
479
- async function handleRefreshQuota() {
480
- setBusy('refresh-quota')
481
- setErr('')
482
- setMsg('')
483
- try {
484
- const res = await fetch(`${ROUTE_PREFIX}/usage`)
485
- const data = await res.json().catch(() => ({}))
486
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
487
- setStatus((prev) => ({ ...prev, usage: data }))
488
- setMsg(t('quota.refreshed_msg'))
489
- } catch (e) {
490
- setErr(String(e.message || e))
491
- } finally {
492
- setBusy('')
493
- }
494
- }
495
-
496
- // Sync official models from ClinePass plan
497
- async function handleSyncPlanModels() {
498
- setBusy('sync-models')
499
- setErr('')
500
- setMsg('')
501
- try {
502
- const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
503
- const data = await res.json().catch(() => ({}))
504
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
505
- setMsg(t('models.synced_msg', { total: data.totalModelsCount, discovered: data.discoveredCount }))
506
- await load()
507
- } catch (e) {
508
- setErr(String(e.message || e))
509
- } finally {
510
- setBusy('')
511
- }
512
- }
513
-
514
- // Register/Force Re-sync Provider in DSH
515
- async function handleRegister() {
516
- setBusy('register')
517
- setErr('')
518
- setMsg('')
519
- try {
520
- const res = await fetch(`${ROUTE_PREFIX}/register`, {
521
- method: 'POST',
522
- headers: { 'Content-Type': 'application/json' },
523
- body: JSON.stringify({ models: draft.enabledModels }),
524
- })
525
- const data = await res.json().catch(() => ({}))
526
- if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
527
- setMsg(t('diag.resynced_msg', { count: draft.enabledModels?.length || 0 }))
528
- await load()
529
- } catch (e) {
530
- setErr(String(e.message || e))
531
- } finally {
532
- setBusy('')
533
- }
534
- }
535
-
536
- // Unregister
537
- async function handleUnregister() {
538
- setBusy('unregister')
539
- setErr('')
540
- setMsg('')
541
- try {
542
- const res = await fetch(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
543
- const data = await res.json().catch(() => ({}))
544
- if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
545
- setMsg(t('diag.unregistered_msg'))
546
- await load()
547
- } catch (e) {
548
- setErr(String(e.message || e))
549
- } finally {
550
- setBusy('')
551
- }
552
- }
553
-
554
- // Smoke chat test
555
- async function handleSmoke() {
556
- setBusy('smoke')
557
- setErr('')
558
- setMsg('')
559
- setSmokeResult(null)
560
- try {
561
- const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
562
- method: 'POST',
563
- headers: { 'Content-Type': 'application/json' },
564
- body: JSON.stringify({ model: draft?.defaultModel }),
565
- })
566
- const data = await res.json().catch(() => ({}))
567
- if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
568
- setSmokeResult(data)
569
- setMsg(t('diag.smoke_ok', { latency: data.latencyMs }))
570
- } catch (e) {
571
- setErr(String(e.message || e))
572
- } finally {
573
- setBusy('')
574
- }
575
- }
576
-
577
- // Fast Browser Login
578
- async function handleFastLogin() {
579
- setBusy('fast-login')
580
- setErr('')
581
- setMsg(t('key.login_waiting'))
582
- try {
583
- const res = await fetch(`${ROUTE_PREFIX}/auth/begin`, { method: 'POST' })
584
- const data = await res.json().catch(() => ({}))
585
- if (!data.ok) throw new Error(data.error || 'Failed to initiate login')
586
- if (data.authUrl) {
587
- window.open(data.authUrl, '_blank')
588
- }
589
- } catch (e) {
590
- setErr(String(e.message || e))
591
- } finally {
592
- setBusy('')
593
- }
594
- }
595
-
596
- // Pin active account
597
- async function handlePinAccount(accountEnv) {
598
- if (scope && snapshotStatus === "ready") {
599
- try {
600
- await scope.set("activeAccount", accountEnv)
601
- } catch (e) {
602
- console.warn('[dsh-clinebot] Failed to set activeAccount on scope:', e)
603
- }
604
- }
605
- setBusy(`pin-${accountEnv}`)
606
- setErr('')
607
- try {
608
- const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
609
- method: 'POST',
610
- headers: { 'Content-Type': 'application/json' },
611
- body: JSON.stringify({ account: accountEnv }),
612
- })
613
- const data = await res.json().catch(() => ({}))
614
- if (!data.ok) throw new Error(data.error || 'Failed to switch account')
615
- await load()
616
- } catch (e) {
617
- setErr(String(e.message || e))
618
- } finally {
619
- setBusy('')
620
- }
621
- }
622
-
623
- // Debounced persistence for model exclusion to avoid stuttering and request thrashing
624
- const debounceTimerRef = typeof React.useRef === 'function' ? React.useRef(null) : { current: null }
625
-
626
- React.useEffect(() => {
627
- return () => {
628
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
629
- }
630
- }, [])
631
-
632
- function debounceSaveDisabledModels(nextDisabled) {
633
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
634
- debounceTimerRef.current = setTimeout(async () => {
635
- if (scope && snapshotStatus === 'ready') {
636
- try {
637
- await scope.set('disabledModels', nextDisabled)
638
- } catch (e) {
639
- console.warn('[dsh-clinebot] Failed to set disabledModels on scope:', e)
640
- }
641
- }
642
- try {
643
- const res = await fetch(`${ROUTE_PREFIX}/models/toggle`, {
644
- method: 'POST',
645
- headers: { 'Content-Type': 'application/json' },
646
- body: JSON.stringify({ disabledModels: nextDisabled }),
647
- })
648
- const data = await res.json().catch(() => ({}))
649
- if (!data.ok) {
650
- setErr(data.error || 'Failed to persist disabled models')
651
- }
652
- } catch (e) {
653
- console.warn('[dsh-clinebot] Failed to persist disabled models:', e)
654
- setErr(String(e.message || e))
655
- }
656
- }, 280)
657
- }
658
-
659
- // Toggle model exclusion (disabledModels logic) with instant UI state update & debounced network persist
660
- function handleToggleModel(id) {
661
- if (!draft) return
662
- const currentDisabled = new Set(draft.disabledModels || [])
663
- if (currentDisabled.has(id)) {
664
- currentDisabled.delete(id) // Re-enable
665
- } else {
666
- currentDisabled.add(id) // Disable
667
- }
668
- const nextDisabled = Array.from(currentDisabled)
669
-
670
- // Compute next enabled models
671
- const allIds = (status?.availableModels || []).map((m) => m.id)
672
- const nextEnabled = allIds.filter((mId) => !currentDisabled.has(mId))
673
-
674
- setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
675
- debounceSaveDisabledModels(nextDisabled)
299
+ return class ClientErrorBoundary extends React.Component {
300
+ constructor(props) {
301
+ super(props)
302
+ this.state = { hasError: false, error: null }
676
303
  }
677
-
678
- // Select all / filter models with instant UI state update & debounced network persist
679
- function handleSetModelsFilter(type) {
680
- if (!status?.availableModels) return
681
- const all = status.availableModels
682
- let allowed = new Set()
683
- if (type === 'all') {
684
- allowed = new Set(all.map((m) => m.id))
685
- } else if (type === 'vision') {
686
- allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
687
- } else if (type === 'coding') {
688
- allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
689
- } else if (type === 'recommended') {
690
- allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
691
- }
692
-
693
- const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
694
- const nextEnabled = Array.from(allowed)
695
-
696
- setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
697
- debounceSaveDisabledModels(nextDisabled)
304
+ static getDerivedStateFromError(error) {
305
+ return { hasError: true, error }
698
306
  }
699
-
700
- if (snapshotStatus === 'unavailable') {
701
- return React.createElement(
702
- 'div',
703
- { className: 'cb-page' },
704
- React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
705
- )
307
+ componentDidCatch(error, info) {
308
+ console.warn('[dsh-clinebot] Client render error caught by boundary:', error, info)
706
309
  }
707
-
708
- if (!status || !draft || snapshotStatus === 'loading') {
709
- if (err) {
310
+ render() {
311
+ if (this.state.hasError) {
710
312
  return React.createElement(
711
313
  'div',
712
- { className: 'cb-page' },
713
- React.createElement('div', { className: 'cb-alert cb-alert-err' }, `${t('settings.loading')}: ${err}`),
314
+ { className: 'cb-alert-err' },
315
+ React.createElement('strong', null, '⚠️ ClineBot UI Error: '),
316
+ String(this.state.error?.message || this.state.error || 'Unknown rendering error'),
714
317
  React.createElement(
715
318
  'button',
716
319
  {
717
320
  type: 'button',
718
321
  className: 'cb-btn',
719
- style: { marginTop: '12px', alignSelf: 'flex-start' },
720
- onClick: () => {
721
- setErr('')
722
- load().catch((e) => setErr(String(e.message || e)))
723
- },
322
+ style: { marginLeft: '12px', padding: '2px 8px', fontSize: '11px' },
323
+ onClick: () => this.setState({ hasError: false, error: null }),
724
324
  },
725
- t('settings.retry')
325
+ 'Retry'
726
326
  )
727
327
  )
728
328
  }
729
- return React.createElement('div', { className: 'cb-page' }, t('settings.loading'))
329
+ return this.props.children
730
330
  }
731
-
732
- const healthOk = !!status.health?.ok
733
- const keyPresent = !!status.key?.present
734
- const isRegistered = !!status.isRegistered
735
- const modelsList = status.availableModels || []
736
- const disabledSet = new Set(draft.disabledModels || [])
737
- const enabledCount = modelsList.filter((m) => !disabledSet.has(m.id)).length
738
- const usage = status.usage
739
-
740
- return React.createElement(
331
+ }
332
+ } catch (e) {
333
+ return function FallbackBoundary(props) { return props?.children || null }
334
+ }
335
+ }
336
+ const ErrorBoundary = createErrorBoundary()
337
+
338
+ // --- src/client/progress-bar.js ---
339
+ function formatResetTime(isoString) {
340
+ if (!isoString) return ''
341
+ try {
342
+ const d = new Date(isoString)
343
+ if (Number.isNaN(d.getTime())) return ''
344
+ return d.toLocaleTimeString()
345
+ } catch {
346
+ return ''
347
+ }
348
+ }
349
+
350
+ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
351
+ const pct = Math.max(0, Math.min(100, Math.round(percentUsed || 0)))
352
+ const isWarn = pct >= 80 && pct < 95
353
+ const isBad = pct >= 95
354
+ const barClass = isBad ? 'cb-progress-bar-bad' : isWarn ? 'cb-progress-bar-warn' : 'cb-progress-bar-fill'
355
+ const resetLabel = formatResetTime(resetsAt)
356
+
357
+ return React.createElement(
358
+ 'div',
359
+ { className: 'cb-progress-card' },
360
+ React.createElement(
361
+ 'div',
362
+ { className: 'cb-progress-head' },
363
+ React.createElement('span', { className: 'cb-progress-label' }, label),
364
+ React.createElement(
365
+ 'span',
366
+ { className: 'cb-progress-meta' },
367
+ t('quota.used', { pct }),
368
+ resetLabel ? ` · ${t('quota.reset_at', { time: resetLabel })}` : ''
369
+ )
370
+ ),
371
+ React.createElement(
372
+ 'div',
373
+ { className: 'cb-progress-track' },
374
+ React.createElement('div', {
375
+ className: barClass,
376
+ style: { width: `${pct}%` },
377
+ })
378
+ )
379
+ )
380
+ }
381
+
382
+ // --- src/client/components/update-banner.js ---
383
+ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
384
+ return React.createElement(
385
+ React.Fragment,
386
+ null,
387
+ React.createElement(
388
+ 'div',
389
+ {
390
+ className: 'cb-row',
391
+ style: {
392
+ padding: '10px 14px',
393
+ borderRadius: '8px',
394
+ border: '1px solid var(--dsw-alias-border-l2)',
395
+ background: 'var(--dsw-alias-bg-layer-2)',
396
+ justifyContent: 'space-between',
397
+ }
398
+ },
399
+ React.createElement(
741
400
  'div',
742
- { className: 'cb-page' },
743
-
744
- // Page Header
401
+ { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' } },
402
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
403
+ `v${updateState.currentVersion}`
404
+ ),
405
+ updateState.checking
406
+ ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
407
+ t('update.checking')
408
+ )
409
+ : updateState.updateAvailable
410
+ ? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
411
+ t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
412
+ )
413
+ : React.createElement('span', { className: 'cb-badge cb-badge-ok' },
414
+ '✓ ' + t('update.up_to_date')
415
+ )
416
+ ),
417
+ updateState.updateAvailable
418
+ ? React.createElement(
419
+ 'button',
420
+ {
421
+ type: 'button',
422
+ className: 'cb-btn cb-btn-primary',
423
+ disabled: updateState.updating,
424
+ onClick: handleTriggerUpdate,
425
+ style: { padding: '5px 12px', fontSize: '12px' }
426
+ },
427
+ updateState.updating ? t('update.updating') : t('update.btn')
428
+ )
429
+ : null
430
+ ),
431
+ updateState.notice ? React.createElement('div', { className: 'cb-alert-ok' }, '✓ ' + updateState.notice) : null,
432
+ updateState.error ? React.createElement('div', { className: 'cb-alert-err' }, updateState.error) : null
433
+ )
434
+ }
435
+
436
+ // --- src/client/components/key-section.js ---
437
+ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowKey, busy, draft, handleSaveKey, handleFastLogin, t }) {
438
+ return React.createElement(
439
+ 'div',
440
+ { className: 'cb-section-card' },
441
+ React.createElement('div', { className: 'cb-section-title' }, t('key.title')),
442
+ React.createElement('div', { className: 'cb-section-desc' }, t('key.desc')),
443
+ React.createElement(
444
+ 'div',
445
+ { className: 'cb-input-group' },
446
+ React.createElement('input', {
447
+ className: 'cb-input',
448
+ type: showKey ? 'text' : 'password',
449
+ placeholder: keyPresent ? t('key.placeholder_has') : t('key.placeholder_empty'),
450
+ value: apiKeyInput,
451
+ onChange: (e) => setApiKeyInput(e.target.value),
452
+ }),
453
+ React.createElement(
454
+ 'button',
455
+ {
456
+ type: 'button',
457
+ className: 'cb-btn',
458
+ onClick: () => setShowKey((v) => !v),
459
+ },
460
+ showKey ? t('key.hide') : t('key.show')
461
+ ),
462
+ React.createElement(
463
+ 'button',
464
+ {
465
+ type: 'button',
466
+ className: 'cb-btn cb-btn-primary',
467
+ disabled: !!busy || !String(apiKeyInput || '').trim(),
468
+ onClick: handleSaveKey,
469
+ },
470
+ busy === 'save-key' ? t('key.saving') : t('key.save')
471
+ ),
472
+ React.createElement(
473
+ 'button',
474
+ {
475
+ type: 'button',
476
+ className: 'cb-btn',
477
+ disabled: !!busy,
478
+ onClick: handleFastLogin,
479
+ },
480
+ busy === 'fast-login' ? '…' : t('key.login_fast')
481
+ )
482
+ ),
483
+ React.createElement(
484
+ 'div',
485
+ { className: 'cb-row', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
486
+ React.createElement('span', null, t('key.env_label')),
487
+ React.createElement('code', null, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
488
+ React.createElement('span', null, ' · '),
489
+ React.createElement(
490
+ 'a',
491
+ {
492
+ href: 'https://app.cline.bot',
493
+ target: '_blank',
494
+ rel: 'noreferrer',
495
+ style: { color: 'var(--dsw-alias-state-brand-primary)' },
496
+ },
497
+ t('key.get_key')
498
+ )
499
+ )
500
+ )
501
+ }
502
+
503
+ // --- src/client/components/accounts-section.js ---
504
+ function AccountsSection({ status, busy, handlePinAccount, t }) {
505
+ if (!status.accounts || !status.accounts.length) return null
506
+ return React.createElement(
507
+ 'div',
508
+ { className: 'cb-section-card' },
509
+ React.createElement('div', { className: 'cb-section-title' }, t('accounts.title')),
510
+ React.createElement('div', { className: 'cb-section-desc' }, t('accounts.desc')),
511
+ React.createElement(
512
+ 'table',
513
+ { className: 'cb-table' },
514
+ React.createElement(
515
+ 'thead',
516
+ null,
745
517
  React.createElement(
746
- 'div',
747
- { className: 'cb-header' },
748
- React.createElement(
749
- 'div',
750
- { className: 'cb-page-title' },
751
- `🤖 ${t('header.title')}`,
752
- React.createElement(
753
- 'span',
754
- { className: `cb-badge ${healthOk ? 'cb-badge-ok' : 'cb-badge-bad'}` },
755
- healthOk ? t('badge.online', { latency: status.health?.latencyMs }) : t('badge.offline')
756
- ),
518
+ 'tr',
519
+ null,
520
+ React.createElement('th', null, 'Account'),
521
+ React.createElement('th', null, 'Credential Ref'),
522
+ React.createElement('th', null, 'Status'),
523
+ React.createElement('th', { style: { textAlign: 'right' } }, 'Action')
524
+ )
525
+ ),
526
+ React.createElement(
527
+ 'tbody',
528
+ null,
529
+ status.accounts.map((acc) => {
530
+ const isActive = status.activeAccount === acc.apiKeyEnv || (!status.activeAccount && acc.id === 'default')
531
+ return React.createElement(
532
+ 'tr',
533
+ { key: acc.id },
534
+ React.createElement('td', null, React.createElement('strong', null, acc.label)),
535
+ React.createElement('td', null, React.createElement('code', null, acc.apiKeyEnv)),
757
536
  React.createElement(
758
- 'span',
759
- { className: `cb-badge ${keyPresent ? 'cb-badge-ok' : 'cb-badge-warn'}` },
760
- keyPresent ? t('badge.key_ok', { source: status.key?.source }) : t('badge.key_missing')
537
+ 'td',
538
+ null,
539
+ acc.present
540
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, `Configured (${acc.source})`)
541
+ : React.createElement('span', { className: 'cb-badge cb-badge-warn' }, 'Missing Key'),
542
+ isActive
543
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('accounts.active_badge'))
544
+ : null
761
545
  ),
762
546
  React.createElement(
763
- 'span',
764
- { className: `cb-badge ${isRegistered ? 'cb-badge-ok' : 'cb-badge-warn'}` },
765
- isRegistered ? t('badge.registered', { count: enabledCount }) : t('badge.not_registered')
547
+ 'td',
548
+ { style: { textAlign: 'right' } },
549
+ !isActive && acc.present
550
+ ? React.createElement(
551
+ 'button',
552
+ {
553
+ type: 'button',
554
+ className: 'cb-btn',
555
+ style: { padding: '4px 8px', fontSize: '11px' },
556
+ disabled: !!busy,
557
+ onClick: () => handlePinAccount(acc.apiKeyEnv),
558
+ },
559
+ t('accounts.pin_btn')
560
+ )
561
+ : null
766
562
  )
767
- ),
563
+ )
564
+ })
565
+ )
566
+ )
567
+ )
568
+ }
569
+
570
+ // --- src/client/components/quota-section.js ---
571
+ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }) {
572
+ return React.createElement(
573
+ React.Fragment,
574
+ null,
575
+ status.quotaWarning
576
+ ? React.createElement(
577
+ 'div',
578
+ { className: status.quotaWarning.level === 'exhausted' ? 'cb-banner-exhausted' : 'cb-banner-warning' },
579
+ React.createElement('span', { style: { fontSize: '16px' } }, status.quotaWarning.level === 'exhausted' ? '🚨' : '⚠️'),
768
580
  React.createElement(
769
581
  'div',
770
- { className: 'cb-page-sub' },
771
- t('header.sub')
582
+ { style: { flex: 1 } },
583
+ React.createElement('div', null, status.quotaWarning.message),
584
+ status.quotaWarning.resetsAt
585
+ ? React.createElement(
586
+ 'div',
587
+ { style: { fontSize: '11px', opacity: 0.9, marginTop: '2px' } },
588
+ t('quota.reset_at', { time: new Date(status.quotaWarning.resetsAt).toLocaleTimeString() })
589
+ )
590
+ : null
772
591
  )
773
- ),
774
-
775
- // Notifications
776
- err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
777
- msg ? React.createElement('div', { className: 'cb-alert-ok' }, msg) : null,
778
-
779
- // Card 1: API Key and Credentials
780
- React.createElement(
592
+ )
593
+ : null,
594
+ keyPresent
595
+ ? React.createElement(
781
596
  'div',
782
597
  { className: 'cb-section-card' },
783
598
  React.createElement(
784
599
  'div',
785
600
  { className: 'cb-section-title' },
786
- t('key.title')
787
- ),
788
- React.createElement(
789
- 'div',
790
- { className: 'cb-section-desc' },
791
- t('key.desc')
792
- ),
793
- React.createElement(
794
- 'div',
795
- { className: 'cb-input-group' },
796
- React.createElement('input', {
797
- className: 'cb-input',
798
- type: showKey ? 'text' : 'password',
799
- placeholder: keyPresent ? t('key.placeholder_has') : t('key.placeholder_empty'),
800
- value: apiKeyInput,
801
- onChange: (e) => setApiKeyInput(e.target.value),
802
- }),
803
- React.createElement(
804
- 'button',
805
- {
806
- type: 'button',
807
- className: 'cb-btn',
808
- onClick: () => setShowKey((v) => !v),
809
- },
810
- showKey ? t('key.hide') : t('key.show')
811
- ),
812
- React.createElement(
813
- 'button',
814
- {
815
- type: 'button',
816
- className: 'cb-btn cb-btn-primary',
817
- disabled: !!busy || !apiKeyInput.trim(),
818
- onClick: handleSaveKey,
819
- },
820
- busy === 'save-key' ? t('key.saving') : t('key.save')
821
- ),
601
+ t('quota.title'),
822
602
  React.createElement(
823
603
  'button',
824
604
  {
825
605
  type: 'button',
826
606
  className: 'cb-btn',
827
607
  disabled: !!busy,
828
- onClick: handleFastLogin,
829
- },
830
- busy === 'fast-login' ? '…' : t('key.login_fast')
831
- )
832
- ),
833
- React.createElement(
834
- 'div',
835
- { className: 'cb-row', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
836
- React.createElement('span', null, t('key.env_label')),
837
- React.createElement('code', null, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
838
- React.createElement('span', null, ` · `),
839
- React.createElement(
840
- 'a',
841
- {
842
- href: 'https://app.cline.bot',
843
- target: '_blank',
844
- rel: 'noreferrer',
845
- style: { color: 'var(--dsw-alias-state-brand-primary)' },
608
+ onClick: handleRefreshQuota,
846
609
  },
847
- t('key.get_key')
848
- )
849
- )
850
- ),
851
-
852
- // Accounts Pool Card
853
- status.accounts && status.accounts.length > 0
854
- ? React.createElement(
855
- 'div',
856
- { className: 'cb-section-card' },
857
- React.createElement(
858
- 'div',
859
- { className: 'cb-section-title' },
860
- t('accounts.title')
861
- ),
862
- React.createElement(
863
- 'div',
864
- { className: 'cb-section-desc' },
865
- t('accounts.desc')
866
- ),
867
- React.createElement(
868
- 'table',
869
- { className: 'cb-table' },
870
- React.createElement(
871
- 'thead',
872
- null,
873
- React.createElement(
874
- 'tr',
875
- null,
876
- React.createElement('th', null, 'Account'),
877
- React.createElement('th', null, 'Credential Ref'),
878
- React.createElement('th', null, 'Status'),
879
- React.createElement('th', { style: { textAlign: 'right' } }, 'Action')
880
- )
881
- ),
882
- React.createElement(
883
- 'tbody',
884
- null,
885
- status.accounts.map((acc) => {
886
- const isActive = status.activeAccount === acc.apiKeyEnv || (!status.activeAccount && acc.id === 'default')
887
- return React.createElement(
888
- 'tr',
889
- { key: acc.id },
890
- React.createElement('td', null, React.createElement('strong', null, acc.label)),
891
- React.createElement('td', null, React.createElement('code', null, acc.apiKeyEnv)),
892
- React.createElement(
893
- 'td',
894
- null,
895
- acc.present
896
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, `Configured (${acc.source})`)
897
- : React.createElement('span', { className: 'cb-badge cb-badge-warn' }, 'Missing Key'),
898
- isActive
899
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('accounts.active_badge'))
900
- : null
901
- ),
902
- React.createElement(
903
- 'td',
904
- { style: { textAlign: 'right' } },
905
- !isActive && acc.present
906
- ? React.createElement(
907
- 'button',
908
- {
909
- type: 'button',
910
- className: 'cb-btn',
911
- style: { padding: '4px 8px', fontSize: '11px' },
912
- disabled: !!busy,
913
- onClick: () => handlePinAccount(acc.apiKeyEnv),
914
- },
915
- t('accounts.pin_btn')
916
- )
917
- : null
918
- )
919
- )
920
- })
921
- )
922
- )
923
- )
924
- : null,
925
-
926
- // Quota Warning Banner
927
- status.quotaWarning
928
- ? React.createElement(
929
- 'div',
930
- { className: status.quotaWarning.level === 'exhausted' ? 'cb-banner-exhausted' : 'cb-banner-warning' },
931
- React.createElement('span', { style: { fontSize: '16px' } }, status.quotaWarning.level === 'exhausted' ? '🚨' : '⚠️'),
932
- React.createElement(
933
- 'div',
934
- { style: { flex: 1 } },
935
- React.createElement('div', null, status.quotaWarning.message),
936
- status.quotaWarning.resetsAt
937
- ? React.createElement(
938
- 'div',
939
- { style: { fontSize: '11px', opacity: 0.9, marginTop: '2px' } },
940
- t('quota.reset_at', { time: new Date(status.quotaWarning.resetsAt).toLocaleTimeString() })
941
- )
942
- : null
943
- )
944
- )
945
- : null,
946
-
947
- // Card 2: Subscription Limits & Usage Dashboard
948
- keyPresent
949
- ? React.createElement(
950
- 'div',
951
- { className: 'cb-section-card' },
952
- React.createElement(
953
- 'div',
954
- { className: 'cb-section-title' },
955
- t('quota.title'),
956
- React.createElement(
957
- 'button',
958
- {
959
- type: 'button',
960
- className: 'cb-btn',
961
- disabled: !!busy,
962
- onClick: handleRefreshQuota,
963
- },
964
- busy === 'refresh-quota' ? t('quota.refreshing') : t('quota.refresh')
965
- )
966
- ),
967
- React.createElement(
968
- 'div',
969
- { className: 'cb-section-desc' },
970
- usage?.user?.email
971
- ? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass ($9.99/mo)' })
972
- : t('quota.desc')
973
- ),
974
- React.createElement(
975
- 'div',
976
- { className: 'cb-grid-2' },
977
- React.createElement(ProgressBar, {
978
- label: t('quota.window_5h'),
979
- percentUsed: usage?.windows?.fiveHour?.percentUsed || 0,
980
- remainingPercent: usage?.windows?.fiveHour?.remainingPercent || 100,
981
- resetsAt: usage?.windows?.fiveHour?.resetsAt,
982
- t,
983
- }),
984
- React.createElement(ProgressBar, {
985
- label: t('quota.window_weekly'),
986
- percentUsed: usage?.windows?.weekly?.percentUsed || 0,
987
- remainingPercent: usage?.windows?.weekly?.remainingPercent || 100,
988
- resetsAt: usage?.windows?.weekly?.resetsAt,
989
- t,
990
- })
991
- )
992
- )
993
- : null,
994
-
995
- // Card 3: Model Picker Management with Official Plan Sync & Auto-Enable
996
- React.createElement(
997
- 'div',
998
- { className: 'cb-section-card' },
999
- React.createElement(
1000
- 'div',
1001
- { className: 'cb-section-title' },
1002
- t('models.title', { enabled: enabledCount, total: modelsList.length }),
1003
- React.createElement(
1004
- 'div',
1005
- { className: 'cb-row' },
1006
- React.createElement(
1007
- 'button',
1008
- {
1009
- type: 'button',
1010
- className: 'cb-btn',
1011
- disabled: !!busy || !keyPresent,
1012
- onClick: handleSyncPlanModels,
1013
- },
1014
- busy === 'sync-models' ? t('models.syncing') : t('models.sync')
1015
- ),
1016
- React.createElement(
1017
- 'button',
1018
- { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') },
1019
- t('models.all')
1020
- ),
1021
- React.createElement(
1022
- 'button',
1023
- { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('vision') },
1024
- t('models.vision')
1025
- ),
1026
- React.createElement(
1027
- 'button',
1028
- { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('coding') },
1029
- t('models.coding')
1030
- ),
1031
- React.createElement(
1032
- 'button',
1033
- { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('recommended') },
1034
- t('models.recommended')
1035
- )
610
+ busy === 'refresh-quota' ? t('quota.refreshing') : t('quota.refresh')
1036
611
  )
1037
612
  ),
1038
613
  React.createElement(
1039
614
  'div',
1040
615
  { className: 'cb-section-desc' },
1041
- t('models.desc')
616
+ usage?.user?.email
617
+ ? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass ($9.99/mo)' })
618
+ : t('quota.desc')
1042
619
  ),
1043
620
  React.createElement(
1044
- 'table',
1045
- { className: 'cb-table' },
1046
- React.createElement(
1047
- 'thead',
1048
- null,
1049
- React.createElement(
1050
- 'tr',
1051
- null,
1052
- React.createElement('th', { style: { width: '40px' } }, t('models.th_active')),
1053
- React.createElement('th', null, t('models.th_name')),
1054
- React.createElement('th', null, t('models.th_id')),
1055
- React.createElement('th', null, t('models.th_ctx')),
1056
- React.createElement('th', null, t('models.th_caps'))
1057
- )
1058
- ),
1059
- React.createElement(
1060
- 'tbody',
1061
- null,
1062
- modelsList.map((m) => {
1063
- const isEnabled = !disabledSet.has(m.id)
1064
- return React.createElement(
1065
- 'tr',
1066
- { key: m.id },
1067
- React.createElement(
1068
- 'td',
1069
- null,
1070
- React.createElement('input', {
1071
- type: 'checkbox',
1072
- checked: isEnabled,
1073
- onChange: () => handleToggleModel(m.id),
1074
- })
1075
- ),
1076
- React.createElement(
1077
- 'td',
1078
- null,
1079
- React.createElement('strong', null, m.name),
1080
- m.recommended
1081
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('models.star'))
1082
- : null,
1083
- m.isCustom
1084
- ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '6px', opacity: 0.8 } }, t('models.auto_new'))
1085
- : null
1086
- ),
1087
- React.createElement('td', null, React.createElement('code', null, m.id)),
1088
- React.createElement('td', null, `${Math.round((m.contextLength || 200000) / 1000)}k`),
1089
- React.createElement(
1090
- 'td',
1091
- null,
1092
- React.createElement('span', { className: 'cb-badge' }, m.category || 'general'),
1093
- m.input?.includes('image') || m.input?.includes('vision')
1094
- ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
1095
- : null,
1096
- m.reasoningEfforts?.length || m.reasoning
1097
- ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, '🧠 Reasoning')
1098
- : null
1099
- )
1100
- )
1101
- })
1102
- )
621
+ 'div',
622
+ { className: 'cb-grid-2' },
623
+ React.createElement(ProgressBar, {
624
+ label: t('quota.window_5h'),
625
+ percentUsed: usage?.windows?.fiveHour?.percentUsed || 0,
626
+ remainingPercent: usage?.windows?.fiveHour?.remainingPercent || 100,
627
+ resetsAt: usage?.windows?.fiveHour?.resetsAt,
628
+ t,
629
+ }),
630
+ React.createElement(ProgressBar, {
631
+ label: t('quota.window_weekly'),
632
+ percentUsed: usage?.windows?.weekly?.percentUsed || 0,
633
+ remainingPercent: usage?.windows?.weekly?.remainingPercent || 100,
634
+ resetsAt: usage?.windows?.weekly?.resetsAt,
635
+ t,
636
+ })
1103
637
  )
638
+ )
639
+ : null
640
+ )
641
+ }
642
+
643
+ // --- src/client/components/models-section.js ---
644
+ function ModelsSection({ modelsList, disabledSet, enabledCount, keyPresent, busy, handleSyncPlanModels, handleSetModelsFilter, handleToggleModel, t }) {
645
+ return React.createElement(
646
+ 'div',
647
+ { className: 'cb-section-card' },
648
+ React.createElement(
649
+ 'div',
650
+ { className: 'cb-section-title' },
651
+ t('models.title', { enabled: enabledCount, total: modelsList.length }),
652
+ React.createElement(
653
+ 'div',
654
+ { className: 'cb-row' },
655
+ React.createElement(
656
+ 'button',
657
+ {
658
+ type: 'button',
659
+ className: 'cb-btn',
660
+ disabled: !!busy || !keyPresent,
661
+ onClick: handleSyncPlanModels,
662
+ },
663
+ busy === 'sync-models' ? t('models.syncing') : t('models.sync')
1104
664
  ),
1105
-
1106
- // Card 4: Session Metrics & Usage Tracking
665
+ React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') }, t('models.all')),
666
+ React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('vision') }, t('models.vision')),
667
+ React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('coding') }, t('models.coding')),
668
+ React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('recommended') }, t('models.recommended'))
669
+ )
670
+ ),
671
+ React.createElement('div', { className: 'cb-section-desc' }, t('models.desc')),
672
+ React.createElement(
673
+ 'table',
674
+ { className: 'cb-table' },
675
+ React.createElement(
676
+ 'thead',
677
+ null,
1107
678
  React.createElement(
1108
- 'div',
1109
- { className: 'cb-section-card' },
1110
- React.createElement(
1111
- 'div',
1112
- { className: 'cb-section-title' },
1113
- t('stats.title')
1114
- ),
1115
- React.createElement(
1116
- 'div',
1117
- { className: 'cb-section-desc' },
1118
- t('stats.desc')
1119
- ),
1120
- React.createElement(
1121
- 'div',
1122
- { className: 'cb-grid-2' },
1123
- React.createElement(
1124
- 'div',
1125
- { className: 'cb-stat-box' },
1126
- React.createElement('div', { className: 'cb-stat-val' }, `${status.sessionStats?.successfulRequests || 0} / ${status.sessionStats?.totalRequests || 0}`),
1127
- React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.requests'))
1128
- ),
679
+ 'tr',
680
+ null,
681
+ React.createElement('th', { style: { width: '40px' } }, t('models.th_active')),
682
+ React.createElement('th', null, t('models.th_name')),
683
+ React.createElement('th', null, t('models.th_id')),
684
+ React.createElement('th', null, t('models.th_ctx')),
685
+ React.createElement('th', null, t('models.th_caps'))
686
+ )
687
+ ),
688
+ React.createElement(
689
+ 'tbody',
690
+ null,
691
+ modelsList.map((m) => {
692
+ const isEnabled = !disabledSet.has(m.id)
693
+ return React.createElement(
694
+ 'tr',
695
+ { key: m.id },
1129
696
  React.createElement(
1130
- 'div',
1131
- { className: 'cb-stat-box' },
1132
- React.createElement('div', { className: 'cb-stat-val' }, `~${status.sessionStats?.totalTokensEst || 0}`),
1133
- React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.tokens'))
697
+ 'td',
698
+ null,
699
+ React.createElement('input', {
700
+ type: 'checkbox',
701
+ checked: isEnabled,
702
+ onChange: () => handleToggleModel(m.id),
703
+ })
1134
704
  ),
1135
705
  React.createElement(
1136
- 'div',
1137
- { className: 'cb-stat-box' },
1138
- React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs} ms` : '—'),
1139
- React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.latency'))
706
+ 'td',
707
+ null,
708
+ React.createElement('strong', null, m.name),
709
+ m.recommended
710
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('models.star'))
711
+ : null,
712
+ m.isCustom
713
+ ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '6px', opacity: 0.8 } }, t('models.auto_new'))
714
+ : null
1140
715
  ),
716
+ React.createElement('td', null, React.createElement('code', null, m.id)),
717
+ React.createElement('td', null, `${Math.round((m.contextLength || 200000) / 1000)}k`),
1141
718
  React.createElement(
1142
- 'div',
1143
- { className: 'cb-stat-box' },
1144
- React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastRequestAt ? new Date(status.sessionStats.lastRequestAt).toLocaleTimeString() : '—'),
1145
- React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.last_req'))
719
+ 'td',
720
+ null,
721
+ React.createElement('span', { className: 'cb-badge' }, m.category || 'general'),
722
+ m.input?.includes('image') || m.input?.includes('vision')
723
+ ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
724
+ : null,
725
+ m.reasoningEfforts?.length || m.reasoning
726
+ ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, '🧠 Reasoning')
727
+ : null
1146
728
  )
1147
729
  )
1148
- ),
1149
-
1150
- // Card 5: Diagnostics & Sync with DSH
1151
- React.createElement(
730
+ })
731
+ )
732
+ )
733
+ )
734
+ }
735
+
736
+ // --- src/client/components/stats-section.js ---
737
+ function StatsSection({ status, t }) {
738
+ return React.createElement(
739
+ 'div',
740
+ { className: 'cb-section-card' },
741
+ React.createElement('div', { className: 'cb-section-title' }, t('stats.title')),
742
+ React.createElement('div', { className: 'cb-section-desc' }, t('stats.desc')),
743
+ React.createElement(
744
+ 'div',
745
+ { className: 'cb-grid-2' },
746
+ React.createElement(
747
+ 'div',
748
+ { className: 'cb-stat-box' },
749
+ React.createElement('div', { className: 'cb-stat-val' }, `${status.sessionStats?.successfulRequests || 0} / ${status.sessionStats?.totalRequests || 0}`),
750
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.requests'))
751
+ ),
752
+ React.createElement(
753
+ 'div',
754
+ { className: 'cb-stat-box' },
755
+ React.createElement('div', { className: 'cb-stat-val' }, `~${status.sessionStats?.totalTokensEst || 0}`),
756
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.tokens'))
757
+ ),
758
+ React.createElement(
759
+ 'div',
760
+ { className: 'cb-stat-box' },
761
+ React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs} ms` : '—'),
762
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.latency'))
763
+ ),
764
+ React.createElement(
765
+ 'div',
766
+ { className: 'cb-stat-box' },
767
+ React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastRequestAt ? new Date(status.sessionStats.lastRequestAt).toLocaleTimeString() : '—'),
768
+ React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.last_req'))
769
+ )
770
+ )
771
+ )
772
+ }
773
+
774
+ // --- src/client/components/diag-section.js ---
775
+ function DiagSection({ keyPresent, isRegistered, busy, smokeResult, handleSmoke, handleUnregister, handleRegister, t }) {
776
+ return React.createElement(
777
+ 'div',
778
+ { className: 'cb-section-card' },
779
+ React.createElement('div', { className: 'cb-section-title' }, t('diag.title')),
780
+ React.createElement('div', { className: 'cb-section-desc' }, t('diag.desc')),
781
+ React.createElement(
782
+ 'div',
783
+ { className: 'cb-row' },
784
+ React.createElement(
785
+ 'button',
786
+ {
787
+ type: 'button',
788
+ className: 'cb-btn',
789
+ disabled: !!busy || !keyPresent,
790
+ onClick: handleSmoke,
791
+ },
792
+ busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
793
+ ),
794
+ isRegistered
795
+ ? React.createElement(
796
+ 'button',
797
+ {
798
+ type: 'button',
799
+ className: 'cb-btn cb-btn-danger',
800
+ disabled: !!busy,
801
+ onClick: handleUnregister,
802
+ },
803
+ busy === 'unregister' ? t('diag.unregistering') : t('diag.unregister_btn')
804
+ )
805
+ : null,
806
+ React.createElement(
807
+ 'button',
808
+ {
809
+ type: 'button',
810
+ className: 'cb-btn cb-btn-primary',
811
+ style: { marginLeft: 'auto' },
812
+ disabled: !!busy,
813
+ onClick: handleRegister,
814
+ },
815
+ busy === 'register' ? t('diag.resyncing') : t('diag.resync_btn')
816
+ )
817
+ ),
818
+ smokeResult
819
+ ? React.createElement(
1152
820
  'div',
1153
- { className: 'cb-section-card' },
1154
- React.createElement(
1155
- 'div',
1156
- { className: 'cb-section-title' },
1157
- t('diag.title')
1158
- ),
1159
- React.createElement(
1160
- 'div',
1161
- { className: 'cb-section-desc' },
1162
- t('diag.desc')
1163
- ),
1164
- React.createElement(
1165
- 'div',
1166
- { className: 'cb-row' },
1167
- React.createElement(
1168
- 'button',
1169
- {
1170
- type: 'button',
1171
- className: 'cb-btn',
1172
- disabled: !!busy || !keyPresent,
1173
- onClick: handleSmoke,
1174
- },
1175
- busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
1176
- ),
1177
- isRegistered
1178
- ? React.createElement(
1179
- 'button',
1180
- {
1181
- type: 'button',
1182
- className: 'cb-btn cb-btn-danger',
1183
- disabled: !!busy,
1184
- onClick: handleUnregister,
1185
- },
1186
- busy === 'unregister' ? t('diag.unregistering') : t('diag.unregister_btn')
1187
- )
1188
- : null,
1189
- React.createElement(
1190
- 'button',
1191
- {
1192
- type: 'button',
1193
- className: 'cb-btn cb-btn-primary',
1194
- style: { marginLeft: 'auto' },
1195
- disabled: !!busy,
1196
- onClick: handleRegister,
1197
- },
1198
- busy === 'register' ? t('diag.resyncing') : t('diag.resync_btn')
1199
- )
1200
- ),
1201
- smokeResult
1202
- ? React.createElement(
1203
- 'div',
1204
- { className: 'cb-preview' },
1205
- `✅ Latency: ${smokeResult.latencyMs} ms | Model: ${smokeResult.model}\nResponse: ${smokeResult.preview || '(empty)'}`
1206
- )
1207
- : null
821
+ { className: 'cb-preview' },
822
+ `✅ Latency: ${smokeResult.latencyMs} ms | Model: ${smokeResult.model}
823
+ Response: ${smokeResult.preview || '(empty)'}`
1208
824
  )
1209
- )
825
+ : null
826
+ )
827
+ }
828
+
829
+ // --- src/client/settings-page.js ---
830
+ function SettingsPage(props) {
831
+ const ctx = props?.ctx
832
+ const t = props?.t || makeT(en, en)
833
+
834
+ const scope = React.useMemo(() => {
835
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
836
+ if (!s?.bind) return undefined
837
+ try {
838
+ return s.bind({ namespace: NS })
839
+ } catch (_) {
840
+ return undefined
841
+ }
842
+ }, [ctx])
843
+
844
+ const subscribe = React.useMemo(() => {
845
+ return (cb) => {
846
+ if (!scope?.subscribe) return () => {}
847
+ try {
848
+ return scope.subscribe(cb) || (() => {})
849
+ } catch (_) {
850
+ return () => {}
851
+ }
852
+ }
853
+ }, [scope])
854
+
855
+ const getSnapshot = React.useCallback(() => {
856
+ if (!scope?.getSnapshot) return SNAPSHOT_LOADING
857
+ try {
858
+ return scope.getSnapshot() || SNAPSHOT_LOADING
859
+ } catch (_) {
860
+ return SNAPSHOT_LOADING
861
+ }
862
+ }, [scope])
863
+
864
+ const snapshot = React.useSyncExternalStore(
865
+ subscribe,
866
+ getSnapshot,
867
+ React.useCallback(() => SNAPSHOT_LOADING, [])
868
+ )
869
+ const snapshotStatus = snapshot?.status || 'loading'
870
+
871
+ const [status, setStatus] = React.useState(null)
872
+ const [draft, setDraft] = React.useState(null)
873
+ const [busy, setBusy] = React.useState('')
874
+ const [err, setErr] = React.useState('')
875
+ const [msg, setMsg] = React.useState('')
876
+ const [smokeResult, setSmokeResult] = React.useState(null)
877
+
878
+ // Plugin in-app updater state
879
+ const [updateState, setUpdateState] = React.useState({
880
+ checking: false,
881
+ updating: false,
882
+ currentVersion: '0.3.12',
883
+ latestVersion: '',
884
+ updateAvailable: false,
885
+ canAutoUpdate: true,
886
+ error: '',
887
+ notice: '',
888
+ })
889
+
890
+ // Key input state
891
+ const [apiKeyInput, setApiKeyInput] = React.useState('')
892
+ const [showKey, setShowKey] = React.useState(false)
893
+
894
+ // Accounts state
895
+ const [newAccountLabel, setNewAccountLabel] = React.useState('')
896
+ const [newAccountEnv, setNewAccountEnv] = React.useState('')
897
+ const [showAddAccount, setShowAddAccount] = React.useState(false)
898
+
899
+ // Filter & Search for Models
900
+ const [modelsFilter, setModelsFilter] = React.useState('all')
901
+ const [modelsSearch, setModelsSearch] = React.useState('')
902
+ const [disabledModels, setDisabledModels] = React.useState([])
903
+ const [advancedOpen, setAdvancedOpen] = React.useState(false)
904
+
905
+ // Loopback fast login state
906
+ const [fastLoginActive, setFastLoginActive] = React.useState(false)
907
+ const [fastLoginNotice, setFastLoginNotice] = React.useState('')
908
+
909
+ React.useEffect(() => {
910
+ ensureCss()
911
+ }, [])
912
+
913
+ const load = React.useCallback(async () => {
914
+ setErr('')
915
+ const res = await fetch(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
916
+ const data = await res.json().catch(() => ({}))
917
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
918
+ setStatus(data)
919
+ setDraft(data.config || {})
920
+ }, [])
921
+
922
+ React.useEffect(() => {
923
+ load().catch((e) => setErr(String(e.message || e)))
924
+ }, [load])
925
+
926
+ const checkUpdate = React.useCallback(async () => {
927
+ setUpdateState((s) => ({ ...s, checking: true, error: '' }))
928
+ try {
929
+ const res = await fetch(`${ROUTE_PREFIX}/update`)
930
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
931
+ const data = await res.json().catch(() => ({}))
932
+ setUpdateState((s) => ({
933
+ ...s,
934
+ checking: false,
935
+ currentVersion: data.currentVersion || s.currentVersion,
936
+ latestVersion: data.latestVersion || '',
937
+ updateAvailable: !!data.updateAvailable,
938
+ canAutoUpdate: data.canAutoUpdate !== false,
939
+ }))
940
+ } catch (_) {
941
+ setUpdateState((s) => ({ ...s, checking: false }))
942
+ }
943
+ }, [])
944
+
945
+ React.useEffect(() => {
946
+ checkUpdate()
947
+ }, [checkUpdate])
948
+
949
+ async function handleTriggerUpdate() {
950
+ if (updateState.updating) return
951
+ setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
952
+ try {
953
+ const res = await fetch(`${ROUTE_PREFIX}/update`, {
954
+ method: 'POST',
955
+ headers: { 'x-dsh-plugin-update': '1' },
956
+ })
957
+ const data = await res.json().catch(() => ({}))
958
+ if (!res.ok || data.ok === false || data.error) {
959
+ throw new Error(data.error || `HTTP ${res.status}`)
960
+ }
961
+ const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion
962
+ setUpdateState((s) => ({
963
+ ...s,
964
+ updating: false,
965
+ updateAvailable: false,
966
+ currentVersion: newVer,
967
+ notice: t('update.done', { version: newVer }),
968
+ }))
969
+ setTimeout(() => checkUpdate(), 2000)
970
+ } catch (err) {
971
+ setUpdateState((s) => ({
972
+ ...s,
973
+ updating: false,
974
+ error: t('update.failed', { error: String(err.message || err) }),
975
+ }))
976
+ }
977
+ }
978
+
979
+ async function handleSaveKey() {
980
+ const keyVal = String(apiKeyInput || '').trim()
981
+ if (!keyVal) {
982
+ setErr(t('key.empty_err'))
983
+ return
984
+ }
985
+ setBusy('save-key')
986
+ setErr('')
987
+ setMsg('')
988
+ try {
989
+ const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
990
+ method: 'POST',
991
+ headers: { 'Content-Type': 'application/json' },
992
+ body: JSON.stringify({ apiKey: keyVal, apiKeyEnv: draft?.apiKeyEnv }),
993
+ })
994
+ const data = await res.json().catch(() => ({}))
995
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
996
+ setApiKeyInput('')
997
+ setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : 'Notice (check console)' }))
998
+ await load()
999
+ } catch (e) {
1000
+ setErr(String(e.message || e))
1001
+ } finally {
1002
+ setBusy('')
1003
+ }
1004
+ }
1005
+
1006
+ async function handleRefreshQuota() {
1007
+ setBusy('refresh-quota')
1008
+ setErr('')
1009
+ setMsg('')
1010
+ try {
1011
+ const res = await fetch(`${ROUTE_PREFIX}/usage`)
1012
+ const data = await res.json().catch(() => ({}))
1013
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1014
+ setStatus((prev) => (prev ? { ...prev, usage: data } : prev))
1015
+ setMsg(t('quota.refreshed_msg'))
1016
+ } catch (e) {
1017
+ setErr(String(e.message || e))
1018
+ } finally {
1019
+ setBusy('')
1020
+ }
1021
+ }
1022
+
1023
+ async function handleSyncPlanModels() {
1024
+ setBusy('sync-models')
1025
+ setErr('')
1026
+ setMsg('')
1027
+ try {
1028
+ const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
1029
+ const data = await res.json().catch(() => ({}))
1030
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1031
+ setMsg(t('models.synced_msg', { total: data.totalModelsCount, discovered: data.discoveredCount }))
1032
+ await load()
1033
+ } catch (e) {
1034
+ setErr(String(e.message || e))
1035
+ } finally {
1036
+ setBusy('')
1037
+ }
1038
+ }
1039
+
1040
+ async function handleRegister() {
1041
+ setBusy('register')
1042
+ setErr('')
1043
+ setMsg('')
1044
+ try {
1045
+ const res = await fetch(`${ROUTE_PREFIX}/register`, { method: 'POST' })
1046
+ const data = await res.json().catch(() => ({}))
1047
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1048
+ const count = status?.availableModels?.filter((m) => !(draft?.disabledModels || []).includes(m.id)).length || 0
1049
+ setMsg(t('diag.resynced_msg', { count }))
1050
+ await load()
1051
+ } catch (e) {
1052
+ setErr(String(e.message || e))
1053
+ } finally {
1054
+ setBusy('')
1055
+ }
1056
+ }
1057
+
1058
+ async function handleUnregister() {
1059
+ setBusy('unregister')
1060
+ setErr('')
1061
+ setMsg('')
1062
+ try {
1063
+ const res = await fetch(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
1064
+ const data = await res.json().catch(() => ({}))
1065
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1066
+ setMsg(t('diag.unregistered_msg'))
1067
+ await load()
1068
+ } catch (e) {
1069
+ setErr(String(e.message || e))
1070
+ } finally {
1071
+ setBusy('')
1072
+ }
1073
+ }
1074
+
1075
+ async function handleSmoke() {
1076
+ setBusy('smoke')
1077
+ setErr('')
1078
+ setSmokeResult(null)
1079
+ try {
1080
+ const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
1081
+ method: 'POST',
1082
+ headers: { 'Content-Type': 'application/json' },
1083
+ body: JSON.stringify({ model: draft?.defaultModel }),
1084
+ })
1085
+ const data = await res.json().catch(() => ({}))
1086
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1087
+ setSmokeResult(data)
1088
+ } catch (e) {
1089
+ setErr(String(e.message || e))
1090
+ } finally {
1091
+ setBusy('')
1092
+ }
1093
+ }
1094
+
1095
+ async function handleFastLogin() {
1096
+ setBusy('fast-login')
1097
+ setErr('')
1098
+ try {
1099
+ const res = await fetch(`${ROUTE_PREFIX}/auth/begin`, { method: 'POST' })
1100
+ const data = await res.json().catch(() => ({}))
1101
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1102
+ if (data.authUrl) {
1103
+ window.open(data.authUrl, '_blank')
1104
+ }
1105
+ } catch (e) {
1106
+ setErr(String(e.message || e))
1107
+ } finally {
1108
+ setBusy('')
1210
1109
  }
1110
+ }
1211
1111
 
1212
- // Accordion item for Plugins section
1213
- function PluginCard(props) {
1214
- const [open, setOpen] = React.useState(false)
1215
- const t = props?.t || makeT(en, en)
1216
- React.useEffect(() => {
1217
- ensureCss()
1218
- }, [])
1112
+ async function handlePinAccount(accountEnv) {
1113
+ setBusy('pin-account')
1114
+ setErr('')
1115
+ try {
1116
+ const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
1117
+ method: 'POST',
1118
+ headers: { 'Content-Type': 'application/json' },
1119
+ body: JSON.stringify({ account: accountEnv }),
1120
+ })
1121
+ const data = await res.json().catch(() => ({}))
1122
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
1123
+ await load()
1124
+ } catch (e) {
1125
+ setErr(String(e.message || e))
1126
+ } finally {
1127
+ setBusy('')
1128
+ }
1129
+ }
1130
+
1131
+ const debounceTimerRef = typeof React.useRef === 'function' ? React.useRef(null) : { current: null }
1132
+ function debounceSaveDisabledModels(nextDisabled) {
1133
+ if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
1134
+ debounceTimerRef.current = setTimeout(async () => {
1135
+ try {
1136
+ await fetch(`${ROUTE_PREFIX}/models/toggle`, {
1137
+ method: 'POST',
1138
+ headers: { 'Content-Type': 'application/json' },
1139
+ body: JSON.stringify({ disabledModels: nextDisabled }),
1140
+ })
1141
+ } catch (err) {
1142
+ console.warn('[dsh-clinebot] Failed saving disabled models:', err)
1143
+ }
1144
+ }, 400)
1145
+ }
1146
+
1147
+ function handleToggleModel(id) {
1148
+ const curr = draft?.disabledModels || []
1149
+ const isCurrentlyDisabled = curr.includes(id)
1150
+ const next = isCurrentlyDisabled ? curr.filter((x) => x !== id) : [...curr, id]
1151
+ const all = status?.availableModels || []
1152
+ const enabledIds = all.map((m) => m.id).filter((mId) => !next.includes(mId))
1153
+ setDraft({ ...draft, disabledModels: next, enabledModels: enabledIds })
1154
+ debounceSaveDisabledModels(next)
1155
+ }
1156
+
1157
+ function handleSetModelsFilter(type) {
1158
+ const all = status?.availableModels || []
1159
+ let allowed = new Set()
1160
+ if (type === 'all') {
1161
+ allowed = new Set(all.map((m) => m.id))
1162
+ } else if (type === 'vision') {
1163
+ allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
1164
+ } else if (type === 'coding') {
1165
+ allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
1166
+ } else if (type === 'recommended') {
1167
+ allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
1168
+ }
1219
1169
 
1170
+ const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
1171
+ const nextEnabled = Array.from(allowed)
1172
+ setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
1173
+ debounceSaveDisabledModels(nextDisabled)
1174
+ }
1175
+
1176
+ if (snapshotStatus === 'unavailable') {
1177
+ return React.createElement(
1178
+ 'div',
1179
+ { className: 'cb-page' },
1180
+ React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
1181
+ )
1182
+ }
1183
+
1184
+ if (!status || !draft || snapshotStatus === 'loading') {
1185
+ if (err) {
1220
1186
  return React.createElement(
1221
- 'li',
1222
- { className: 'cb-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
1187
+ 'div',
1188
+ { className: 'cb-page' },
1189
+ React.createElement('div', { className: 'cb-alert cb-alert-err' }, `${t('settings.loading')}: ${err}`),
1223
1190
  React.createElement(
1224
1191
  'button',
1225
1192
  {
1226
1193
  type: 'button',
1227
- style: {
1228
- background: 'none',
1229
- border: 'none',
1230
- cursor: 'pointer',
1231
- display: 'flex',
1232
- alignItems: 'center',
1233
- width: '100%',
1234
- padding: 0,
1235
- textAlign: 'left',
1194
+ className: 'cb-btn',
1195
+ style: { marginTop: '12px', alignSelf: 'flex-start' },
1196
+ onClick: () => {
1197
+ setErr('')
1198
+ load().catch((e) => setErr(String(e.message || e)))
1236
1199
  },
1237
- 'aria-expanded': open,
1238
- onClick: () => setOpen((v) => !v),
1239
1200
  },
1240
- React.createElement(
1241
- 'div',
1242
- { style: { flex: 1 } },
1243
- React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
1244
- React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
1245
- ),
1246
- React.createElement('span', { className: 'cb-chevron' + (open ? ' cb-chevron-open' : '') },
1247
- React.createElement(Chevron)
1248
- )
1249
- ),
1250
- open
1251
- ? React.createElement(
1252
- 'div',
1253
- { style: { marginTop: '16px' } },
1254
- React.createElement(
1255
- ErrorBoundary,
1256
- null,
1257
- React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx })
1258
- )
1259
- )
1260
- : null
1201
+ t('settings.retry')
1202
+ )
1261
1203
  )
1262
1204
  }
1205
+ return React.createElement('div', { className: 'cb-page' }, t('settings.loading'))
1206
+ }
1207
+
1208
+ const healthOk = !!status.health?.ok
1209
+ const keyPresent = !!status.key?.present
1210
+ const isRegistered = !!status.isRegistered
1211
+ const modelsList = status.availableModels || []
1212
+ const disabledSet = new Set(draft.disabledModels || [])
1213
+ const enabledCount = modelsList.filter((m) => !disabledSet.has(m.id)).length
1214
+ const usage = status.usage
1215
+
1216
+ return React.createElement(
1217
+ 'div',
1218
+ { className: 'cb-page' },
1219
+
1220
+ // Page Header
1221
+ React.createElement(
1222
+ 'div',
1223
+ { className: 'cb-header' },
1224
+ React.createElement(
1225
+ 'div',
1226
+ { className: 'cb-page-title' },
1227
+ `🤖 ${t('header.title')}`,
1228
+ React.createElement(
1229
+ 'span',
1230
+ { className: `cb-badge ${healthOk ? 'cb-badge-ok' : 'cb-badge-bad'}` },
1231
+ healthOk ? t('badge.online', { latency: status.health?.latencyMs }) : t('badge.offline')
1232
+ ),
1233
+ React.createElement(
1234
+ 'span',
1235
+ { className: `cb-badge ${keyPresent ? 'cb-badge-ok' : 'cb-badge-warn'}` },
1236
+ keyPresent ? t('badge.key_ok', { source: status.key?.source }) : t('badge.key_missing')
1237
+ ),
1238
+ React.createElement(
1239
+ 'span',
1240
+ { className: `cb-badge ${isRegistered ? 'cb-badge-ok' : 'cb-badge-warn'}` },
1241
+ isRegistered ? t('badge.registered', { count: enabledCount }) : t('badge.not_registered')
1242
+ )
1243
+ ),
1244
+ React.createElement('div', { className: 'cb-page-sub' }, t('header.sub'))
1245
+ ),
1246
+
1247
+ // In-app Update Bar
1248
+ React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, t }),
1249
+
1250
+ // Notifications
1251
+ err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
1252
+ msg ? React.createElement('div', { className: 'cb-alert-ok' }, msg) : null,
1253
+
1254
+ // Card 1: API Key and Credentials
1255
+ React.createElement(KeySection, {
1256
+ keyPresent,
1257
+ apiKeyInput,
1258
+ setApiKeyInput,
1259
+ showKey,
1260
+ setShowKey,
1261
+ busy,
1262
+ draft,
1263
+ handleSaveKey,
1264
+ handleFastLogin,
1265
+ t,
1266
+ }),
1267
+
1268
+ // Accounts Pool Card
1269
+ React.createElement(AccountsSection, { status, busy, handlePinAccount, t }),
1270
+
1271
+ // Quota Warning & Dashboard Card
1272
+ React.createElement(QuotaSection, { keyPresent, status, usage, busy, handleRefreshQuota, t }),
1273
+
1274
+ // Card 3: Model Picker Management
1275
+ React.createElement(ModelsSection, {
1276
+ modelsList,
1277
+ disabledSet,
1278
+ enabledCount,
1279
+ keyPresent,
1280
+ busy,
1281
+ handleSyncPlanModels,
1282
+ handleSetModelsFilter,
1283
+ handleToggleModel,
1284
+ t,
1285
+ }),
1286
+
1287
+ // Card 4: Session Metrics & Usage Tracking
1288
+ React.createElement(StatsSection, { status, t }),
1289
+
1290
+ // Card 5: Diagnostics & Sync with DSH
1291
+ React.createElement(DiagSection, {
1292
+ keyPresent,
1293
+ isRegistered,
1294
+ busy,
1295
+ smokeResult,
1296
+ handleSmoke,
1297
+ handleUnregister,
1298
+ handleRegister,
1299
+ t,
1300
+ })
1301
+ )
1302
+ }
1303
+
1304
+ // --- src/client/plugin-card.js ---
1305
+ function PluginCard(props) {
1306
+ const [open, setOpen] = React.useState(false)
1307
+ const t = props?.t || makeT(en, en)
1308
+ React.useEffect(() => {
1309
+ ensureCss()
1310
+ }, [])
1311
+
1312
+ return React.createElement(
1313
+ 'li',
1314
+ { className: 'cb-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
1315
+ React.createElement(
1316
+ 'button',
1317
+ {
1318
+ type: 'button',
1319
+ style: {
1320
+ background: 'none',
1321
+ border: 'none',
1322
+ cursor: 'pointer',
1323
+ display: 'flex',
1324
+ alignItems: 'center',
1325
+ width: '100%',
1326
+ padding: 0,
1327
+ textAlign: 'left',
1328
+ },
1329
+ 'aria-expanded': open,
1330
+ onClick: () => setOpen((v) => !v),
1331
+ },
1332
+ React.createElement(
1333
+ 'div',
1334
+ { style: { flex: 1 } },
1335
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
1336
+ React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
1337
+ ),
1338
+ React.createElement('span', { className: 'cb-chevron' + (open ? ' cb-chevron-open' : '') },
1339
+ React.createElement(Chevron)
1340
+ )
1341
+ ),
1342
+ open
1343
+ ? React.createElement(
1344
+ 'div',
1345
+ { style: { marginTop: '16px' } },
1346
+ React.createElement(
1347
+ ErrorBoundary,
1348
+ null,
1349
+ React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx })
1350
+ )
1351
+ )
1352
+ : null
1353
+ )
1354
+ }
1263
1355
 
1264
- function refreshMirrorUntilVisible(ctx) {
1265
- const visible = () => {
1266
- try {
1267
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1268
- const view = s?.describe?.()?.getSnapshot?.()?.view
1269
- return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
1270
- } catch (_) {
1271
- return false
1272
- }
1273
- }
1274
- if (visible()) return () => {}
1275
- let tries = 0
1276
- const timer = setInterval(() => {
1277
- if (visible() || tries >= 15) { clearInterval(timer); return }
1278
- tries += 1
1279
- try {
1280
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1281
- s?.describe?.()?.load?.()
1282
- } catch (e) {
1283
- console.debug?.('[dsh-clinebot] Polling settings mirror:', e)
1284
- }
1285
- }, 1000)
1286
- return () => clearInterval(timer)
1356
+ // --- src/client/entry.js ---
1357
+ function refreshMirrorUntilVisible(ctx) {
1358
+ const visible = () => {
1359
+ try {
1360
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1361
+ const view = s?.describe?.()?.getSnapshot?.()?.view
1362
+ return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
1363
+ } catch (_) {
1364
+ return false
1287
1365
  }
1366
+ }
1367
+ if (visible()) return () => {}
1368
+ let tries = 0
1369
+ const timer = setInterval(() => {
1370
+ if (visible() || tries >= 15) { clearInterval(timer); return }
1371
+ tries += 1
1372
+ try {
1373
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1374
+ s?.describe?.()?.load?.()
1375
+ } catch (e) {
1376
+ console.debug?.('[dsh-clinebot] Polling settings mirror:', e)
1377
+ }
1378
+ }, 1000)
1379
+ return () => clearInterval(timer)
1380
+ }
1288
1381
 
1289
- function apply(ctx) {
1290
- const addLocale = (locale, dictionary) => {
1291
- try {
1292
- return ctx.locale.register(NS, locale, dictionary)
1293
- } catch (_) {
1294
- return () => {}
1295
- }
1296
- }
1297
- if (ctx.locale && ctx.locale.register) {
1298
- if (typeof ctx.effect === 'function') {
1299
- ctx.effect(() => {
1300
- const undo = [addLocale('en', en), addLocale('zh', zh)]
1301
- return () => { for (const off of undo) off() }
1302
- }, 'dsh-clinebot: dictionaries')
1303
- } else {
1304
- addLocale('en', en)
1305
- addLocale('zh', zh)
1306
- }
1307
- }
1308
-
1309
- if (typeof ctx.effect === 'function') {
1310
- ctx.effect(
1311
- () => refreshMirrorUntilVisible(ctx),
1312
- 'dsh-clinebot: re-read the settings mirror until our namespace appears',
1313
- )
1314
- }
1315
-
1316
- function registerSlotWhenReady(slotName, registerFn) {
1317
- if (!ctx.slots) return
1318
- if (typeof ctx.slots.inject === 'function') {
1319
- try {
1320
- ctx.slots.inject(slotName, () => {
1321
- try {
1322
- return registerFn()
1323
- } catch (err) {
1324
- console.warn('[dsh-clinebot] Error registering slot ' + slotName + ':', err)
1325
- }
1326
- })
1327
- return
1328
- } catch (err) {
1329
- console.warn('[dsh-clinebot] Failed to inject slot ' + slotName + ':', err)
1330
- }
1331
- }
1332
- if (typeof ctx.slots.register === 'function') {
1382
+ function apply(ctx) {
1383
+ const addLocale = (locale, dictionary) => {
1384
+ try {
1385
+ return ctx.locale.register(NS, locale, dictionary)
1386
+ } catch (_) {
1387
+ return () => {}
1388
+ }
1389
+ }
1390
+ if (ctx.locale && ctx.locale.register) {
1391
+ if (typeof ctx.effect === 'function') {
1392
+ ctx.effect(() => {
1393
+ const undo = [addLocale('en', en), addLocale('zh', zh)]
1394
+ return () => { for (const off of undo) off() }
1395
+ }, 'dsh-clinebot: dictionaries')
1396
+ } else {
1397
+ addLocale('en', en)
1398
+ addLocale('zh', zh)
1399
+ }
1400
+ }
1401
+
1402
+ if (typeof ctx.effect === 'function') {
1403
+ ctx.effect(
1404
+ () => refreshMirrorUntilVisible(ctx),
1405
+ 'dsh-clinebot: re-read the settings mirror until our namespace appears',
1406
+ )
1407
+ }
1408
+
1409
+ function registerSlotWhenReady(slotName, registerFn) {
1410
+ if (!ctx.slots) return
1411
+ if (typeof ctx.slots.inject === 'function') {
1412
+ try {
1413
+ ctx.slots.inject(slotName, () => {
1333
1414
  try {
1334
- registerFn()
1415
+ return registerFn()
1335
1416
  } catch (err) {
1336
- console.warn('[dsh-clinebot] Failed direct registration for ' + slotName + ':', err)
1417
+ console.warn('[dsh-clinebot] Error registering slot ' + slotName + ':', err)
1337
1418
  }
1338
- }
1419
+ })
1420
+ return
1421
+ } catch (err) {
1422
+ console.warn('[dsh-clinebot] Failed to inject slot ' + slotName + ':', err)
1423
+ }
1424
+ }
1425
+ if (typeof ctx.slots.register === 'function') {
1426
+ try {
1427
+ registerFn()
1428
+ } catch (err) {
1429
+ console.warn('[dsh-clinebot] Failed direct registration for ' + slotName + ':', err)
1339
1430
  }
1340
-
1341
- registerSlotWhenReady('settings.plugin.item', () =>
1342
- ctx.slots.register(
1343
- {
1344
- name: 'settings.plugin.item',
1345
- key: NS,
1346
- locale: NS,
1347
- inject: () => ({ ctx }),
1348
- },
1349
- (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1350
- )
1351
- )
1352
1431
  }
1432
+ }
1433
+
1434
+ registerSlotWhenReady('settings.plugin.item', () =>
1435
+ ctx.slots.register(
1436
+ {
1437
+ name: 'settings.plugin.item',
1438
+ key: NS,
1439
+ locale: NS,
1440
+ inject: () => ({ ctx }),
1441
+ },
1442
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1443
+ )
1444
+ )
1445
+ }
1446
+
1447
+ module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
1353
1448
 
1354
- module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
1355
1449
  return module.exports
1356
1450
  },
1357
1451
  })