@goodandready/dsh-clinebot 0.3.11 → 0.3.13

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